├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── Format.py ├── HostfileConvert.sh ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── RadiumBrowser.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── RadiumBrowser.xcscheme └── xcuserdata │ └── bslayter.xcuserdatad │ └── xcschemes │ ├── RadiumBrowser.xcscheme │ └── xcschememanagement.plist ├── RadiumBrowser.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── RadiumBrowser ├── AdBlockManager.swift ├── AddBookmarkTableViewController.swift ├── AddressBar.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── .DS_Store │ ├── AppIcon.appiconset │ │ ├── .DS_Store │ │ ├── 1024Icon copy.png │ │ ├── Contents.json │ │ ├── Icon29 copy.png │ │ ├── Icon29.png │ │ ├── Icon29@2x-1.png │ │ ├── Icon29@2x.png │ │ ├── Icon29@3x.png │ │ ├── Icon40-1.png │ │ ├── Icon40-2.png │ │ ├── Icon40.png │ │ ├── Icon40@2x-1.png │ │ ├── Icon40@2x.png │ │ ├── Icon40@3x.png │ │ ├── Icon60@2x.png │ │ ├── Icon60@3x.png │ │ ├── Icon76.png │ │ ├── Icon76@2x.png │ │ ├── Icon83.5@2x copy-1.png │ │ ├── Icon83.5@2x copy.png │ │ └── Icon83.5@2x.png │ ├── Contents.json │ ├── NoAds.imageset │ │ ├── Contents.json │ │ └── NoAds.png │ ├── back.imageset │ │ ├── Back_000000_25.png │ │ ├── Back_000000_50.png │ │ ├── Back_000000_75.png │ │ └── Contents.json │ ├── forward.imageset │ │ ├── Contents.json │ │ ├── Forward_000000_25.png │ │ ├── Forward_000000_50.png │ │ └── Forward_000000_75.png │ ├── globe.imageset │ │ ├── Contents.json │ │ ├── Globe_000000_25.png │ │ ├── Globe_000000_50.png │ │ └── Globe_000000_75.png │ └── menu.imageset │ │ ├── Contents.json │ │ └── menu.png ├── AutocompleteTableViewCell.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Bookmark.swift ├── BookmarkCollectionViewCell.swift ├── BookmarkCollectionViewController.swift ├── BookmarkTableViewCell.swift ├── BrowsingSession.swift ├── BuiltinExtension.swift ├── CloudToButt.js ├── DoneAccessoryView.swift ├── ExtensionModel.swift ├── ExtensionsTableViewController.swift ├── FaviconGetter.swift ├── Favicons.js ├── HistoryEntry.swift ├── HistoryTableViewController.swift ├── Info.plist ├── Main.storyboard ├── MainViewController.swift ├── MigrationManager.swift ├── NotificationExtensions.swift ├── OrderedSet.swift ├── RadiumBrowser-Bridging-Header.h ├── ScriptEditorViewController.swift ├── SettingsTableViewController.swift ├── SharedConfig.swift ├── SharedDropdownMenu.swift ├── SharedTextField.swift ├── StringExtensions.swift ├── SuggestionManager.swift ├── TabCollectionViewCell.swift ├── TabContainerView.swift ├── TabCountButton.swift ├── TabTrayViewController.swift ├── TabView.swift ├── UIImageExtensions.swift ├── UIViewExtensions.swift ├── WKWebViewExtensions.swift ├── WebContainer.swift ├── WebServer.swift ├── WebViewManager.swift ├── adServerHosts.json ├── adaway.json ├── blackHosts.json ├── camelon.json ├── malwareHosts.json ├── simpleAds.json ├── topdomains.txt ├── tracker.json ├── ultimateAdBlock.json └── zeus.json ├── RadiumBrowserTests ├── Info.plist └── RadiumBrowserTests.swift ├── RadiumBrowserUITests ├── Info.plist └── RadiumBrowserUITests.swift └── fastlane ├── Appfile ├── Fastfile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | # Swift Package Manager 31 | # 32 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 33 | # Packages/ 34 | .build/ 35 | 36 | # CocoaPods 37 | # 38 | # We recommend against adding the Pods directory to your .gitignore. However 39 | # you should judge for yourself, the pros and cons are mentioned at: 40 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 41 | # 42 | Pods/ 43 | 44 | # Carthage 45 | # 46 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 47 | # Carthage/Checkouts 48 | 49 | Carthage/Build 50 | 51 | # fastlane 52 | # 53 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 54 | # screenshots whenever they are needed. 55 | # For more information about the recommended setup visit: 56 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 57 | fastlane/report.xml 58 | fastlane/screenshots 59 | fastlane/Preview.html 60 | fastlane/test_output 61 | test_output/ 62 | *.app.dSYM.zip 63 | report.xml 64 | hosts 65 | 66 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - variable_name 4 | - valid_docs 5 | - shorthand_operator 6 | - void_return 7 | - redundant_discardable_let 8 | - unused_optional_binding 9 | 10 | excluded: 11 | - Pods 12 | - AxxessCARETests 13 | - DuctTapeUITests 14 | 15 | force_cast: warning 16 | 17 | line_length: 18 | - 150 # Warning 19 | 20 | function_body_length: 21 | - 150 # Warning 22 | - 250 # Error 23 | 24 | reporter: "checkstyle" 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | xcode_workspace: RadiumBrowser.xcworkspace # path to your xcodeproj folder 3 | xcode_scheme: RadiumBrowser 4 | -------------------------------------------------------------------------------- /Format.py: -------------------------------------------------------------------------------- 1 | lines = [line.rstrip('\n') for line in open('hosts')] 2 | 3 | output = '[{"trigger":{"url-filter":".*","if-domain":[' 4 | for line in lines: 5 | output += '"' + line + '",' 6 | 7 | output = output[:-1] 8 | output += ']},"action":{"type":"block"}}]' 9 | 10 | print output 11 | -------------------------------------------------------------------------------- /HostfileConvert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to convert a standard host file into a ad block list we can use in Radium 3 | 4 | read -p 'Host file location: ' hostloc 5 | read -p 'Output file name: ' outputfile 6 | 7 | curl -ls $hostloc | grep -E '^[0-9]' | grep -v -E '\s*localhost\s*$' | perl -pe 's/^[0-9.]+\s(\S+)\s*.*$/"$1",/mg' | perl -pe 's/\n//g' | perl -pe 's/\A(.+),\z/[{"trigger":{"url-filter":".*","if-domain":[$1]},"action":{"type":"block"}}]/g' | tr "[:upper:]" "[:lower:]" > $outputfile 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Brad Slayter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '10.0' 4 | inhibit_all_warnings! 5 | use_frameworks! 6 | 7 | pre_install do |installer| 8 | # workaround for https://github.com/CocoaPods/CocoaPods/issues/3289 9 | def installer.verify_no_static_framework_transitive_dependencies; end 10 | end 11 | 12 | def all_pods 13 | # See "Shared" folder for more shared libraries/resources 14 | 15 | # AutoLayout 16 | pod 'SnapKit' 17 | # Then API - initialization 18 | pod 'Then' 19 | # Realm database 20 | pod 'RealmSwift' 21 | # async images 22 | pod 'SDWebImage', '~> 3.8' 23 | # Color utilities 24 | pod 'BSColorUtils', :git => 'https://github.com/SlayterDev/BSColorUtils' 25 | # UITextView Syntax Highlighting 26 | pod 'Highlightr', :git => 'https://github.com/raspu/Highlightr.git' 27 | # Event based actions 28 | pod 'LUAutocompleteView' 29 | pod 'GCDWebServer', '~> 3.0' 30 | pod 'SwiftyStoreKit' 31 | pod 'BulletinBoard' 32 | pod 'SwiftKeychainWrapper' 33 | 34 | project 'RadiumBrowser.xcodeproj' 35 | 36 | end 37 | 38 | target 'RadiumBrowser' do 39 | all_pods 40 | end 41 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - BSColorUtils (1.0.3) 3 | - BulletinBoard (4.1.0) 4 | - GCDWebServer (3.5.4): 5 | - GCDWebServer/Core (= 3.5.4) 6 | - GCDWebServer/Core (3.5.4) 7 | - Highlightr (2.1.0) 8 | - LUAutocompleteView (4.0.0) 9 | - Realm (4.4.1): 10 | - Realm/Headers (= 4.4.1) 11 | - Realm/Headers (4.4.1) 12 | - RealmSwift (4.4.1): 13 | - Realm (= 4.4.1) 14 | - SDWebImage (3.8.3): 15 | - SDWebImage/Core (= 3.8.3) 16 | - SDWebImage/Core (3.8.3) 17 | - SnapKit (5.0.1) 18 | - SwiftKeychainWrapper (3.4.0) 19 | - SwiftyStoreKit (0.15.0) 20 | - Then (2.6.0) 21 | 22 | DEPENDENCIES: 23 | - BSColorUtils (from `https://github.com/SlayterDev/BSColorUtils`) 24 | - BulletinBoard 25 | - GCDWebServer (~> 3.0) 26 | - Highlightr (from `https://github.com/raspu/Highlightr.git`) 27 | - LUAutocompleteView 28 | - RealmSwift 29 | - SDWebImage (~> 3.8) 30 | - SnapKit 31 | - SwiftKeychainWrapper 32 | - SwiftyStoreKit 33 | - Then 34 | 35 | SPEC REPOS: 36 | https://github.com/CocoaPods/Specs.git: 37 | - BulletinBoard 38 | - GCDWebServer 39 | - LUAutocompleteView 40 | - Realm 41 | - RealmSwift 42 | - SDWebImage 43 | - SnapKit 44 | - SwiftKeychainWrapper 45 | - SwiftyStoreKit 46 | - Then 47 | 48 | EXTERNAL SOURCES: 49 | BSColorUtils: 50 | :git: https://github.com/SlayterDev/BSColorUtils 51 | Highlightr: 52 | :git: https://github.com/raspu/Highlightr.git 53 | 54 | CHECKOUT OPTIONS: 55 | BSColorUtils: 56 | :commit: a3cb69c6235054333795c7a58ca943a8c48661bc 57 | :git: https://github.com/SlayterDev/BSColorUtils 58 | Highlightr: 59 | :commit: 87465e7ac116a4c8672fa29aae840e9c9e68bedb 60 | :git: https://github.com/raspu/Highlightr.git 61 | 62 | SPEC CHECKSUMS: 63 | BSColorUtils: 25172e692f3a30152f27ae5108d550b9707ae13d 64 | BulletinBoard: c774340545b5cfd05a96a89f63b105ecc95c86f9 65 | GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4 66 | Highlightr: 683f05d5223cade533a78528a35c9f06e4caddf8 67 | LUAutocompleteView: 604edbbb358d77c8fff4b86137db76a38eb4beda 68 | Realm: 4eb04d7487bd43c0581256f40b424eafb711deff 69 | RealmSwift: 3eb8924ff7100df5928c7602f71d0fec51e7c9c5 70 | SDWebImage: a72e880a8fe0f7fc31efe15aaed443c074d2a80c 71 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 72 | SwiftKeychainWrapper: 6fc49fbf7d4a6b0772917acb0e53a1639f6078d6 73 | SwiftyStoreKit: 033d06b6f87955405d3f844a18a7687c2438592a 74 | Then: 90cd104fd951cec1980a03f57704ad8f784d4d79 75 | 76 | PODFILE CHECKSUM: 63f6ba53394a0412f2a046f7dd4a185d81e92837 77 | 78 | COCOAPODS: 1.9.1 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RADIUM # 2 | 3 | Get it on the [App Store](https://itunes.apple.com/us/app/radium-web-browser/id1274491203?mt=8)! 4 | 5 | ### What is this repository for? ### 6 | 7 | * iOS tabbed web browser with extensions 8 | * Version 1.3 9 | 10 | ### How do I get set up? ### 11 | 12 | * `git clone https://github.com/SlayterDev/RadiumBrowser.git` 13 | * `pod install` 14 | * Build and run :+1: 15 | 16 | ### Discussion ### 17 | 18 | This is a web browser with multiple tabs backed by `WKWebView`. The unique feature of this browser is the ability for the user to add their own JavaScript extensions to the browser. The app is also capable of using its own builtin extensions. Currently the only one is a script to look for the touch icons of a website and return it back to the app to use for the icon on the tab and favorites. The app uses the Realm database to store extensions, history, favorites, and browsing sessions. 19 | 20 | The code editor for the extensions has syntax highlighting and auto indentation. So that's pretty neat. 21 | 22 | ### Contributors ### 23 | 24 | Fork, edit, pull request :+1: 25 | 26 | ![Imgur](http://i.imgur.com/bMQuwAE.png) ![Imgur](http://i.imgur.com/Wjro36A.png) 27 | -------------------------------------------------------------------------------- /RadiumBrowser.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RadiumBrowser.xcodeproj/xcshareddata/xcschemes/RadiumBrowser.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 76 | 78 | 84 | 85 | 86 | 87 | 88 | 89 | 95 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /RadiumBrowser.xcodeproj/xcuserdata/bslayter.xcuserdatad/xcschemes/RadiumBrowser.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 89 | 90 | 91 | 92 | 93 | 94 | 100 | 102 | 108 | 109 | 110 | 111 | 113 | 114 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /RadiumBrowser.xcodeproj/xcuserdata/bslayter.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RadiumBrowser.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 41A567EE1E40CB3B00470017 16 | 17 | primary 18 | 19 | 20 | 41A568021E40CB3B00470017 21 | 22 | primary 23 | 24 | 25 | 41A5680D1E40CB3B00470017 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /RadiumBrowser.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /RadiumBrowser.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RadiumBrowser/AdBlockManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdBlockManager.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 11/4/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import WebKit 11 | import SwiftKeychainWrapper 12 | 13 | class AdBlockManager { 14 | static let shared = AdBlockManager() 15 | 16 | @available(iOS 11.0, *) 17 | func setupAdBlock(forKey key: String, filename: String, webView: WKWebView?, completion: (() -> Void)?) { 18 | if UserDefaults.standard.bool(forKey: key) { 19 | WKContentRuleListStore.default().lookUpContentRuleList(forIdentifier: key) { [weak self] ruleList, error in 20 | if let error = error { 21 | print("\(filename).json" + error.localizedDescription) 22 | UserDefaults.standard.set(false, forKey: key) 23 | self?.setupAdBlock(forKey: key, filename: filename, webView: webView, completion: completion) 24 | return 25 | } 26 | if let list = ruleList { 27 | webView?.configuration.userContentController.add(list) 28 | completion?() 29 | } 30 | } 31 | } else { 32 | if let jsonPath = Bundle.main.path(forResource: filename, ofType: "json"), let jsonContent = try? String(contentsOfFile: jsonPath, encoding: .utf8) { 33 | WKContentRuleListStore.default().compileContentRuleList(forIdentifier: key, encodedContentRuleList: jsonContent) { ruleList, error in 34 | if let error = error { 35 | print("\(filename).json" + error.localizedDescription) 36 | completion?() 37 | return 38 | } 39 | if let list = ruleList { 40 | webView?.configuration.userContentController.add(list) 41 | UserDefaults.standard.set(true, forKey: key) 42 | completion?() 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | @available(iOS 11.0, *) 50 | func setupAdBlockFromStringLiteral(forWebView webView: WKWebView?, completion: (() -> Void)?) { 51 | // Swift 4 Multi-line string literals 52 | let jsonString = """ 53 | [{ 54 | "trigger": { 55 | "url-filter": "://googleads\\\\.g\\\\.doubleclick\\\\.net.*" 56 | }, 57 | "action": { 58 | "type": "block" 59 | } 60 | }] 61 | """ 62 | if UserDefaults.standard.bool(forKey: SettingsKeys.stringLiteralAdBlock) { 63 | // list should already be compiled 64 | WKContentRuleListStore.default().lookUpContentRuleList(forIdentifier: SettingsKeys.stringLiteralAdBlock) { [weak self] (contentRuleList, error) in 65 | if let error = error { 66 | print(error.localizedDescription) 67 | UserDefaults.standard.set(false, forKey: SettingsKeys.stringLiteralAdBlock) 68 | self?.setupAdBlockFromStringLiteral(forWebView: webView, completion: completion) 69 | return 70 | } 71 | if let list = contentRuleList { 72 | webView?.configuration.userContentController.add(list) 73 | completion?() 74 | } 75 | } 76 | } else { 77 | WKContentRuleListStore.default().compileContentRuleList(forIdentifier: SettingsKeys.stringLiteralAdBlock, encodedContentRuleList: jsonString) { contentRuleList, error in 78 | if let error = error { 79 | print(error.localizedDescription) 80 | completion?() 81 | return 82 | } 83 | if let list = contentRuleList { 84 | webView?.configuration.userContentController.add(list) 85 | UserDefaults.standard.set(true, forKey: SettingsKeys.stringLiteralAdBlock) 86 | completion?() 87 | } 88 | } 89 | } 90 | } 91 | 92 | func shouldBlockAds() -> Bool { 93 | let adBlockPurchased = KeychainWrapper.standard.bool(forKey: SettingsKeys.adBlockPurchased) ?? false 94 | return adBlockPurchased && UserDefaults.standard.bool(forKey: SettingsKeys.adBlockEnabled) 95 | } 96 | 97 | @available(iOS 11.0, *) 98 | func disableAdBlock(forWebView webView: WKWebView?) { 99 | webView?.configuration.userContentController.removeAllContentRuleLists() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /RadiumBrowser/AddBookmarkTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookmarkTableViewController.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 2/9/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | 12 | 13 | fileprivate let reuseidentifier = "bookmarkCell" 14 | 15 | class AddBookmarkTableViewController: UITableViewController { 16 | 17 | @objc var pageIconURL: String? 18 | @objc var pageTitle: String? 19 | @objc var pageURL: String? 20 | 21 | @objc var titleTextField: UITextField? 22 | @objc var urlTextField: UITextField? 23 | 24 | override var preferredContentSize: CGSize { 25 | get { 26 | return CGSize(width: 414, height: 200) 27 | } 28 | set { super.preferredContentSize = newValue } 29 | } 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | title = "Add Bookmark" 35 | 36 | self.navigationController?.navigationBar.barTintColor = Colors.radiumGray 37 | 38 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .done, target: self, action: #selector(done)) 39 | self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancel)) 40 | 41 | tableView.register(BookmarkTableViewCell.self, forCellReuseIdentifier: reuseidentifier) 42 | 43 | tableView.rowHeight = UITableView.automaticDimension 44 | tableView.estimatedRowHeight = 88 45 | } 46 | 47 | override func didReceiveMemoryWarning() { 48 | super.didReceiveMemoryWarning() 49 | // Dispose of any resources that can be recreated. 50 | } 51 | 52 | @objc func displayValidationError(for field: String) { 53 | let av = UIAlertController(title: "Error", message: "Please enter a \(field) for your bookmark.", preferredStyle: .alert) 54 | av.addAction(UIAlertAction(title: "Ok", style: .cancel, handler: nil)) 55 | self.present(av, animated: true, completion: nil) 56 | } 57 | 58 | @objc func done() { 59 | guard let title = titleTextField?.text, title != "" else { 60 | displayValidationError(for: "title") 61 | return 62 | } 63 | guard let url = urlTextField?.text, url != "" else { 64 | displayValidationError(for: "URL") 65 | return 66 | } 67 | 68 | let bookmark = Bookmark(value: ["id": UUID().uuidString, "name": title, 69 | "pageURL": url, "iconURL": pageIconURL ?? ""]) 70 | do { 71 | let realm = try Realm() 72 | try realm.write { 73 | realm.add(bookmark) 74 | } 75 | } catch let error { 76 | logRealmError(error: error) 77 | } 78 | 79 | self.dismiss(animated: true, completion: nil) 80 | } 81 | 82 | @objc func cancel() { 83 | self.dismiss(animated: true, completion: nil) 84 | } 85 | 86 | // MARK: - Table view data source 87 | 88 | override func numberOfSections(in tableView: UITableView) -> Int { 89 | return 1 90 | } 91 | 92 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 93 | return 1 94 | } 95 | 96 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 97 | let cell = tableView.dequeueReusableCell(withIdentifier: reuseidentifier, for: indexPath) as? BookmarkTableViewCell 98 | 99 | if let pageIconURL = pageIconURL, let imgURL = URL(string: pageIconURL) { 100 | cell?.imageView?.sd_setImage(with: imgURL, placeholderImage: UIImage(named: "globe")) 101 | } else { 102 | cell?.imageView?.image = UIImage(named: "globe") 103 | } 104 | 105 | titleTextField = cell?.titleTextField 106 | urlTextField = cell?.urlTextField 107 | 108 | cell?.titleTextField?.text = pageTitle 109 | cell?.urlTextField?.text = pageURL 110 | 111 | return cell! 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /RadiumBrowser/AddressBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddressBar.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AddressBar: UIView, UITextFieldDelegate { 12 | 13 | @objc static let standardHeight: CGFloat = 44 14 | 15 | @objc var backButton: UIButton? 16 | @objc var forwardButton: UIButton? 17 | @objc var refreshButton: UIButton? 18 | @objc var addressField: UITextField? 19 | @objc var menuButton: UIButton? 20 | 21 | @objc weak var tabContainer: TabContainerView? 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | 26 | self.backgroundColor = Colors.radiumGray 27 | self.layer.borderColor = UIColor.lightGray.cgColor 28 | self.layer.borderWidth = 0.5 29 | 30 | backButton = UIButton().then { [unowned self] in 31 | $0.setImage(UIImage(named: "back")?.withRenderingMode(.alwaysTemplate), for: .normal) 32 | $0.setTitleColor(.black, for: .normal) 33 | $0.setTitleColor(.lightGray, for: .disabled) 34 | $0.tintColor = .lightGray 35 | $0.isEnabled = false 36 | 37 | self.addSubview($0) 38 | $0.snp.makeConstraints { (make) in 39 | make.left.equalTo(self).offset(8) 40 | make.width.equalTo(32) 41 | make.centerY.equalTo(self) 42 | } 43 | } 44 | 45 | if isiPadUI { 46 | forwardButton = UIButton().then { [unowned self] in 47 | $0.setImage(UIImage(named: "forward")?.withRenderingMode(.alwaysTemplate), for: .normal) 48 | $0.setTitleColor(.black, for: .normal) 49 | $0.setTitleColor(.lightGray, for: .disabled) 50 | $0.isEnabled = false 51 | $0.tintColor = .lightGray 52 | 53 | self.addSubview($0) 54 | $0.snp.makeConstraints { (make) in 55 | make.left.equalTo(self.backButton!.snp.right).offset(8) 56 | make.width.equalTo(32) 57 | make.centerY.equalTo(self) 58 | } 59 | } 60 | } 61 | 62 | menuButton = UIButton().then { [unowned self] in 63 | $0.setImage(UIImage(named: "menu"), for: .normal) 64 | 65 | self.addSubview($0) 66 | $0.snp.makeConstraints { (make) in 67 | make.width.equalTo(25) 68 | make.height.equalTo(25) 69 | make.centerY.equalTo(self) 70 | make.right.equalTo(self).offset(-8) 71 | } 72 | } 73 | 74 | addressField = SharedTextField().then { [unowned self] in 75 | $0.placeholder = "Address" 76 | $0.backgroundColor = .white 77 | $0.layer.borderColor = UIColor.lightGray.cgColor 78 | $0.layer.borderWidth = 0.5 79 | $0.layer.cornerRadius = 4 80 | $0.inset = 8 81 | 82 | $0.autocorrectionType = .no 83 | $0.autocapitalizationType = .none 84 | $0.keyboardType = .webSearch 85 | $0.delegate = self 86 | $0.clearButtonMode = .whileEditing 87 | 88 | if !isiPadUI { 89 | $0.inputAccessoryView = DoneAccessoryView(targetView: $0, width: UIScreen.main.bounds.width).then { obj in 90 | obj.doneButton?.setTitle("Cancel", for: .normal) 91 | obj.doneButton?.snp.updateConstraints { make in 92 | make.width.equalTo(60) 93 | } 94 | } 95 | } 96 | 97 | self.addSubview($0) 98 | $0.snp.makeConstraints { (make) in 99 | if isiPadUI { 100 | make.left.equalTo(self.forwardButton!.snp.right).offset(8) 101 | } else { 102 | make.left.equalTo(self.backButton!.snp.right).offset(8) 103 | } 104 | make.top.equalTo(self).offset(8) 105 | make.bottom.equalTo(self).offset(-8) 106 | make.right.equalTo(self.menuButton!.snp.left).offset(-8) 107 | } 108 | } 109 | 110 | refreshButton = UIButton(frame: CGRect(x: -5, y: 0, width: 12.5, height: 15)).then { 111 | $0.setImage(UIImage.imageFrom(systemItem: .refresh)?.withRenderingMode(.alwaysTemplate), for: .normal) 112 | $0.tintColor = .gray 113 | addressField?.rightView = $0 114 | addressField?.rightViewMode = .unlessEditing 115 | } 116 | } 117 | 118 | required init?(coder aDecoder: NSCoder) { 119 | fatalError("init(coder:) has not been implemented") 120 | } 121 | 122 | // MARK: - Setup 123 | 124 | @objc func setupNaviagtionActions(forTabConatiner tabContainer: TabContainerView) { 125 | backButton?.addTarget(tabContainer, action: #selector(tabContainer.goBack(sender:)), for: .touchUpInside) 126 | forwardButton?.addTarget(tabContainer, action: #selector(tabContainer.goForward(sender:)), for: .touchUpInside) 127 | refreshButton?.addTarget(tabContainer, action: #selector(tabContainer.refresh(sender:)), for: .touchUpInside) 128 | } 129 | 130 | // MARK: - Actions 131 | 132 | @objc func setAddressText(_ text: String?) { 133 | guard let _ = addressField else { return } 134 | 135 | if !addressField!.isFirstResponder { 136 | addressField?.text = text 137 | checkForLocalhost() 138 | } 139 | } 140 | 141 | @objc func setAttributedAddressText(_ text: NSAttributedString) { 142 | guard let _ = addressField else { return } 143 | 144 | if !addressField!.isFirstResponder { 145 | addressField?.attributedText = text 146 | checkForLocalhost() 147 | } 148 | } 149 | 150 | func checkForLocalhost() { 151 | if let address = addressField?.text, address.contains("localhost") { 152 | addressField?.text = "" 153 | } 154 | } 155 | 156 | // MARK: - Textfield Delegate 157 | 158 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 159 | tabContainer?.loadQuery(string: textField.text) 160 | textField.resignFirstResponder() 161 | return true 162 | } 163 | 164 | func textFieldDidBeginEditing(_ textField: UITextField) { 165 | if let string = textField.attributedText?.mutableCopy() as? NSMutableAttributedString { 166 | string.setAttributes(convertToOptionalNSAttributedStringKeyDictionary([:]), range: NSRange(0.. [NSAttributedString.Key: Any]? { 178 | guard let input = input else { return nil } 179 | return Dictionary(uniqueKeysWithValues: input.map { key, value in (NSAttributedString.Key(rawValue: key), value)}) 180 | } 181 | -------------------------------------------------------------------------------- /RadiumBrowser/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Then 11 | import SnapKit 12 | import RealmSwift 13 | import StoreKit 14 | import SwiftyStoreKit 15 | import SwiftKeychainWrapper 16 | 17 | @UIApplicationMain 18 | class AppDelegate: UIResponder, UIApplicationDelegate { 19 | 20 | var window: UIWindow? 21 | 22 | @objc var mainController: MainViewController? 23 | 24 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 25 | 26 | #if arch(i386) || arch(x86_64) 27 | let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] 28 | NSLog("Document Path: %@", documentsPath) 29 | #endif 30 | 31 | SwiftyStoreKit.completeTransactions(atomically: true) { purchases in 32 | for purchase in purchases { 33 | if purchase.transaction.transactionState == .purchased || purchase.transaction.transactionState == .restored { 34 | if purchase.needsFinishTransaction { 35 | KeychainWrapper.standard.set(true, forKey: SettingsKeys.adBlockPurchased) 36 | SwiftyStoreKit.finishTransaction(purchase.transaction) 37 | } 38 | print("purchased: \(purchase)") 39 | } 40 | } 41 | } 42 | 43 | MigrationManager.shared.attemptMigration() 44 | 45 | WebServer.shared.startServer() 46 | 47 | let defaults = UserDefaults.standard 48 | if !defaults.bool(forKey: SettingsKeys.firstRun) { 49 | defaults.set(true, forKey: SettingsKeys.firstRun) 50 | performFirstRunTasks() 51 | } 52 | if defaults.string(forKey: SettingsKeys.searchEngineUrl) == nil { 53 | defaults.set("https://duckduckgo.com/?q=", forKey: SettingsKeys.searchEngineUrl) 54 | } 55 | 56 | #if DEBUG 57 | // KeychainWrapper.standard.set(false, forKey: SettingsKeys.adBlockPurchased) 58 | #endif 59 | defaults.set(false, forKey: SettingsKeys.stringLiteralAdBlock) 60 | for hostFile in HostFileNames.allValues { 61 | defaults.set(false, forKey: hostFile.rawValue) 62 | } 63 | 64 | mainController = MainViewController() 65 | self.window?.rootViewController = mainController 66 | self.window?.makeKeyAndVisible() 67 | 68 | return true 69 | } 70 | 71 | func performFirstRunTasks() { 72 | UserDefaults.standard.set(true, forKey: SettingsKeys.trackHistory) 73 | } 74 | 75 | func applicationWillResignActive(_ application: UIApplication) { 76 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions 77 | // (such as an incoming phone call or SMS message) or when the user quits the application and it begins the 78 | // transition to the background state. 79 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. 80 | // Games should use this method to pause the game. 81 | mainController?.tabContainer?.saveBrowsingSession() 82 | } 83 | 84 | func applicationDidEnterBackground(_ application: UIApplication) { 85 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to 86 | // restore your application to its current state in case it is terminated later. 87 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 88 | } 89 | 90 | func applicationWillEnterForeground(_ application: UIApplication) { 91 | // Called as part of the transition from the background to the active state; 92 | // here you can undo many of the changes made on entering the background. 93 | } 94 | 95 | func applicationDidBecomeActive(_ application: UIApplication) { 96 | // Restart any tasks that were paused (or not yet started) while the application was inactive. 97 | // If the application was previously in the background, optionally refresh the user interface. 98 | } 99 | 100 | func applicationWillTerminate(_ application: UIApplication) { 101 | mainController?.tabContainer?.saveBrowsingSession() 102 | } 103 | 104 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { 105 | do { 106 | let source = try String(contentsOf: url, encoding: .utf8) 107 | mainController?.openEditor(withSource: source, andName: url.deletingPathExtension().lastPathComponent) 108 | } catch { 109 | print("Could not open file") 110 | } 111 | 112 | return true 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/.DS_Store -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/1024Icon copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/1024Icon copy.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon40-2.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "1024Icon copy.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon29@2x-1.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon40@2x-1.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon29 copy.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon40-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon29.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon29@2x.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon40.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon40@2x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "Icon83.5@2x copy-1.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29 copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29 copy.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29@2x-1.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29@2x.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon29@3x.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40-1.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40-2.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40@2x-1.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40@2x.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon40@3x.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon60@2x.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon60@3x.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon76.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon76@2x.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon83.5@2x copy-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon83.5@2x copy-1.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon83.5@2x copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon83.5@2x copy.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/AppIcon.appiconset/Icon83.5@2x.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/NoAds.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "NoAds.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 | } -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/NoAds.imageset/NoAds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/NoAds.imageset/NoAds.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/back.imageset/Back_000000_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/back.imageset/Back_000000_25.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/back.imageset/Back_000000_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/back.imageset/Back_000000_50.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/back.imageset/Back_000000_75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/back.imageset/Back_000000_75.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Back_000000_25.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Back_000000_50.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Back_000000_75.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/forward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Forward_000000_25.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Forward_000000_50.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Forward_000000_75.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/forward.imageset/Forward_000000_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/forward.imageset/Forward_000000_25.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/forward.imageset/Forward_000000_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/forward.imageset/Forward_000000_50.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/forward.imageset/Forward_000000_75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/forward.imageset/Forward_000000_75.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/globe.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Globe_000000_25.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Globe_000000_50.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Globe_000000_75.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/globe.imageset/Globe_000000_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/globe.imageset/Globe_000000_25.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/globe.imageset/Globe_000000_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/globe.imageset/Globe_000000_50.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/globe.imageset/Globe_000000_75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/globe.imageset/Globe_000000_75.png -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/menu.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "menu.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 | } -------------------------------------------------------------------------------- /RadiumBrowser/Assets.xcassets/menu.imageset/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlayterDev/RadiumBrowser/1a1d68e69210044791295f8678157cf87de35bf3/RadiumBrowser/Assets.xcassets/menu.imageset/menu.png -------------------------------------------------------------------------------- /RadiumBrowser/AutocompleteTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutocompleteTableViewCell.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 11/1/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import LUAutocompleteView 11 | 12 | class AutocompleteTableViewCell: LUAutocompleteTableViewCell { 13 | 14 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 15 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) 16 | 17 | detailTextLabel?.textColor = .gray 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | override func set(text: String) { 25 | textLabel?.text = text 26 | DispatchQueue.global().async { 27 | let pageTitle = SuggestionManager.shared.pageTitle(forURLSring: text) 28 | DispatchQueue.main.async { 29 | self.detailTextLabel?.text = pageTitle 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /RadiumBrowser/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 | -------------------------------------------------------------------------------- /RadiumBrowser/Bookmark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bookmark.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/6/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | class Bookmark: Object { 13 | @objc dynamic var id = "" 14 | @objc dynamic var name = "" 15 | @objc dynamic var pageURL = "" 16 | @objc dynamic var iconURL = "" 17 | 18 | override static func indexedProperties() -> [String] { 19 | return ["name"] 20 | } 21 | 22 | override static func primaryKey() -> String? { 23 | return "id" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /RadiumBrowser/BookmarkCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkCollectionViewCell.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/8/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BookmarkCollectionViewCell: UICollectionViewCell { 12 | @objc var textLabel: UILabel? 13 | @objc var imageView: UIImageView? 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | 18 | imageView = UIImageView().then { 19 | $0.contentMode = .scaleAspectFit 20 | 21 | self.contentView.addSubview($0) 22 | $0.snp.makeConstraints { (make) in 23 | make.top.equalTo(self.contentView) 24 | make.left.equalTo(self.contentView) 25 | make.width.equalTo(65) 26 | make.height.equalTo(65) 27 | make.centerX.equalTo(self.contentView) 28 | } 29 | } 30 | 31 | textLabel = UILabel().then { 32 | $0.textAlignment = .center 33 | $0.adjustsFontSizeToFitWidth = true 34 | $0.lineBreakMode = .byTruncatingTail 35 | $0.minimumScaleFactor = 0.8 36 | $0.font = .systemFont(ofSize: 14) 37 | 38 | self.contentView.addSubview($0) 39 | $0.snp.makeConstraints { (make) in 40 | make.top.equalTo(imageView!.snp.bottom).offset(4) 41 | make.left.equalTo(self.contentView) 42 | make.width.equalTo(self.contentView) 43 | make.bottom.equalTo(self.contentView) 44 | } 45 | } 46 | } 47 | 48 | required init?(coder aDecoder: NSCoder) { 49 | fatalError("init(coder:) has not been implemented") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /RadiumBrowser/BookmarkCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkCollectionViewController.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/8/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | 12 | 13 | private let reuseIdentifier = "Cell" 14 | 15 | class BookmarkCollectionViewController: UICollectionViewController { 16 | 17 | @objc var notificationToken: NotificationToken! 18 | var realm: Realm! 19 | 20 | var bookmarks: Results? 21 | 22 | weak var delegate: HistoryNavigationDelegate? 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | title = "Bookmarks" 28 | 29 | self.navigationController?.navigationBar.barTintColor = Colors.radiumGray 30 | 31 | self.collectionView?.backgroundColor = .white 32 | self.collectionView?.contentInset = UIEdgeInsets(top: 16, left: 8, bottom: 0, right: 8) 33 | 34 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(done)) 35 | 36 | // Register cell classes 37 | self.collectionView!.register(BookmarkCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier) 38 | 39 | do { 40 | realm = try Realm() 41 | bookmarks = realm.objects(Bookmark.self) 42 | notificationToken = bookmarks?.observe { [weak self] (changes: RealmCollectionChange) in 43 | guard let collectionView = self?.collectionView else { return } 44 | switch changes { 45 | case .initial: 46 | collectionView.reloadData() 47 | case .update: 48 | collectionView.reloadSections([0]) 49 | case .error(let error): 50 | logRealmError(error: error) 51 | } 52 | } 53 | } catch let error { 54 | logRealmError(error: error) 55 | } 56 | } 57 | 58 | override func didReceiveMemoryWarning() { 59 | super.didReceiveMemoryWarning() 60 | // Dispose of any resources that can be recreated. 61 | } 62 | 63 | @objc func done() { 64 | self.dismiss(animated: true, completion: nil) 65 | } 66 | 67 | // MARK: UICollectionViewDataSource 68 | 69 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 70 | return 1 71 | } 72 | 73 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 74 | return bookmarks?.count ?? 0 75 | } 76 | 77 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 78 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? BookmarkCollectionViewCell 79 | 80 | let bookmark = bookmarks?[indexPath.row] 81 | cell?.textLabel?.text = bookmark?.name 82 | if let iconURL = bookmark?.iconURL, iconURL != "", let imgURL = URL(string: bookmark!.iconURL) { 83 | cell?.imageView?.sd_setImage(with: imgURL, placeholderImage: UIImage(named: "globe")) 84 | } else { 85 | cell?.imageView?.image = UIImage(named: "globe") 86 | } 87 | 88 | return cell! 89 | } 90 | 91 | // MARK: UICollectionViewDelegate 92 | 93 | /* 94 | // Uncomment this method to specify if the specified item should be highlighted during tracking 95 | override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { 96 | return true 97 | } 98 | */ 99 | 100 | // Uncomment this method to specify if the specified item should be selected 101 | override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 102 | return true 103 | } 104 | 105 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 106 | defer { 107 | self.dismiss(animated: true, completion: nil) 108 | } 109 | guard let bookmark = bookmarks?[indexPath.row] else { return } 110 | 111 | delegate?.didSelectEntry(with: URL(string: bookmark.pageURL)) 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /RadiumBrowser/BookmarkTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkTableViewCell.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 2/9/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BookmarkTableViewCell: UITableViewCell { 12 | 13 | @objc var titleTextField: UITextField? 14 | @objc var urlTextField: UITextField? 15 | 16 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 17 | super.init(style: style, reuseIdentifier: reuseIdentifier) 18 | 19 | imageView?.snp.remakeConstraints { (make) in 20 | make.left.equalTo(self.contentView) 21 | make.top.equalTo(self.contentView) 22 | make.width.equalTo(self.contentView.snp.height) 23 | make.height.equalTo(88) 24 | make.bottom.equalTo(self.contentView) 25 | } 26 | 27 | titleTextField = SharedTextField().then { [unowned self] in 28 | $0.inset = 8 29 | $0.placeholder = "Bookmark Name" 30 | $0.clearButtonMode = .always 31 | 32 | self.contentView.addSubview($0) 33 | $0.snp.makeConstraints { (make) in 34 | make.left.equalTo(self.imageView!.snp.right) 35 | make.right.equalTo(self.contentView) 36 | make.top.equalTo(self.contentView) 37 | make.height.equalTo(44) 38 | } 39 | } 40 | 41 | urlTextField = SharedTextField().then { [unowned self] in 42 | $0.inset = 8 43 | $0.placeholder = "Bookmark URL" 44 | $0.clearButtonMode = .always 45 | 46 | self.contentView.addSubview($0) 47 | $0.snp.makeConstraints { (make) in 48 | make.left.equalTo(self.imageView!.snp.right) 49 | make.right.equalTo(self.contentView) 50 | make.top.equalTo(self.titleTextField!.snp.bottom) 51 | make.height.equalTo(44) 52 | } 53 | } 54 | 55 | let _ = UIView().then { [unowned self] in 56 | $0.backgroundColor = .lightGray 57 | 58 | self.contentView.addSubview($0) 59 | $0.snp.makeConstraints { (make) in 60 | make.top.equalTo(self.titleTextField!.snp.bottom) 61 | make.right.equalTo(self.contentView) 62 | make.left.equalTo(self.imageView!.snp.right).offset(8) 63 | make.height.equalTo(0.5) 64 | } 65 | } 66 | } 67 | 68 | required init?(coder aDecoder: NSCoder) { 69 | fatalError("init(coder:) has not been implemented") 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /RadiumBrowser/BrowsingSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowsingSession.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/5/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | class URLModel: Object { 13 | @objc dynamic var urlString = "" 14 | @objc dynamic var pageTitle = "" 15 | } 16 | 17 | class BrowsingSession: Object { 18 | let tabs = List() 19 | @objc dynamic var selectedTabIndex = 0 20 | } 21 | -------------------------------------------------------------------------------- /RadiumBrowser/BuiltinExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuiltinExtension.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 2/8/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebKit 11 | 12 | protocol UserScriptHandler { 13 | var scriptMessageHandler: ScriptHandler { get } 14 | } 15 | 16 | class BuiltinExtension: NSObject { 17 | @objc var extensionName: String { 18 | return "UNNAMED" 19 | } 20 | 21 | @objc var scriptHandlerName: String? 22 | 23 | @objc var webContainer: WebContainer? 24 | @objc var webScript: WKUserScript? 25 | 26 | @objc init(container: WebContainer) { 27 | super.init() 28 | 29 | webContainer = container 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RadiumBrowser/CloudToButt.js: -------------------------------------------------------------------------------- 1 | // Enabling this extension will replace all 2 | // occurrences of "Cloud" with "Butt" 3 | // This is especially fun if you forget 4 | // you've enabled it. Try searching for 5 | // "the cloud" on google to see it in 6 | // action. 7 | 8 | walk(document.body); 9 | 10 | function walk(node) { 11 | // I stole this function from here: 12 | // http://is.gd/mwZp7E 13 | var child, next; 14 | 15 | switch (node.nodeType) { 16 | case 1: // Element 17 | case 9: // Document 18 | case 11: // Document fragment 19 | child = node.firstChild; 20 | while (child) { 21 | next = child.nextSibling; 22 | walk(child); 23 | child = next; 24 | } 25 | break; 26 | case 3: // Text node 27 | handleText(node); 28 | break; 29 | } 30 | } 31 | 32 | function handleText(textNode) { 33 | var v = textNode.nodeValue; 34 | 35 | v = v.replace(/\bThe Cloud\b/g, "My Butt"); 36 | v = v.replace(/\bThe cloud\b/g, "My butt"); 37 | v = v.replace(/\bthe Cloud\b/g, "my Butt"); 38 | v = v.replace(/\bthe cloud\b/g, "my butt"); 39 | v = v.replace(/\bcloud\b/g, "butt"); 40 | v = v.replace(/\bCloud\b/g, "Butt"); 41 | v = v.replace(/\bclouds\b/g, "butts"); 42 | v = v.replace(/\bClouds\b/g, "Butts"); 43 | 44 | textNode.nodeValue = v; 45 | } 46 | -------------------------------------------------------------------------------- /RadiumBrowser/DoneAccessoryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoneAccessoryView.swift 3 | // DuctTape 4 | // 5 | // Created by Brad Slayter on 2/6/17. 6 | // Copyright © 2017 Brad Slayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DoneAccessoryView: UIView { 12 | 13 | @objc var doneButton: UIButton? 14 | @objc var targetView: UIView? 15 | 16 | @objc init(targetView: UIView?, width: CGFloat) { 17 | super.init(frame: CGRect(x: 0, y: 0, width: width, height: 35)) 18 | 19 | self.targetView = targetView 20 | 21 | self.layer.borderColor = UIColor.lightGray.cgColor 22 | self.layer.borderWidth = 0.25 23 | 24 | doneButtonSetup() 25 | } 26 | 27 | required init?(coder aDecoder: NSCoder) { 28 | super.init(coder: aDecoder) 29 | } 30 | 31 | @objc func doneButtonSetup() { 32 | self.backgroundColor = Colors.radiumGray 33 | doneButton = UIButton(type: .custom).then { 34 | $0.setTitle("Done", for: .normal) 35 | $0.setTitleColor(.black, for: .normal) 36 | $0.addTarget(self, action: #selector(dismissKeyboard), for: .touchUpInside) 37 | 38 | self.addSubview($0) 39 | $0.snp.makeConstraints { (make) -> Void in 40 | make.top.equalTo(self) 41 | if #available(iOS 11.0, *) { 42 | make.right.equalTo(self.safeAreaLayoutGuide.snp.right).offset(-8) 43 | } else { 44 | make.right.equalTo(self).offset(-8) 45 | } 46 | make.bottom.equalTo(self) 47 | make.width.equalTo(50.0) 48 | } 49 | } 50 | } 51 | 52 | @objc func dismissKeyboard() { 53 | targetView?.endEditing(true) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /RadiumBrowser/ExtensionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionModel.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 2/2/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | class ExtensionModel: Object { 13 | @objc dynamic var source = "" 14 | @objc dynamic var id = "" 15 | @objc dynamic var name = "" 16 | @objc dynamic var active = true 17 | @objc dynamic var injectionTime = 1 // 0 == Start, 1 == End 18 | 19 | override class func primaryKey() -> String? { 20 | return "id" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RadiumBrowser/ExtensionsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionsViewControllerTableViewController.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 2/2/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | 12 | 13 | class ExtensionsTableViewController: UITableViewController, ScriptEditorDelegate { 14 | 15 | var extensions: Results? 16 | 17 | @objc var notificationToken: NotificationToken! 18 | var realm: Realm! 19 | 20 | deinit { 21 | notificationToken.invalidate() 22 | } 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | title = "Extensions" 28 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 29 | 30 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.done(sender:))) 31 | 32 | do { 33 | self.realm = try Realm() 34 | self.notificationToken = realm.observe { _, _ in 35 | DispatchQueue.main.async { 36 | self.tableView.reloadSections([1], with: .automatic) 37 | } 38 | } 39 | self.extensions = realm.objects(ExtensionModel.self) 40 | } catch let error as NSError { 41 | print("Error occured opening realm: \(error.localizedDescription)") 42 | } 43 | } 44 | 45 | override func didReceiveMemoryWarning() { 46 | super.didReceiveMemoryWarning() 47 | // Dispose of any resources that can be recreated. 48 | } 49 | 50 | @objc func done(sender: UIBarButtonItem) { 51 | self.dismiss(animated: true, completion: nil) 52 | } 53 | 54 | // MARK: - Table view data source 55 | 56 | override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 57 | if indexPath.section == 1 { 58 | return true 59 | } 60 | 61 | return false 62 | } 63 | 64 | override func numberOfSections(in tableView: UITableView) -> Int { 65 | return 2 66 | } 67 | 68 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 69 | if section == 0 { 70 | return 1 71 | } 72 | 73 | return extensions?.count ?? 0 74 | } 75 | 76 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 77 | if section == 1 { 78 | return "Extensions (tap to edit)" 79 | } 80 | 81 | return "" 82 | } 83 | 84 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 85 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 86 | 87 | if indexPath.section == 0 { 88 | cell.textLabel?.text = "Add new extension..." 89 | } else { 90 | cell.accessoryType = .disclosureIndicator 91 | if let item = extensions?[indexPath.row] { 92 | cell.textLabel?.text = item.name 93 | cell.accessoryView = UISwitch().then { [unowned self] in 94 | $0.isOn = item.active 95 | $0.tag = indexPath.row 96 | $0.addTarget(self, action: #selector(self.toggleScript(sender:)), for: .valueChanged) 97 | } 98 | } 99 | } 100 | 101 | return cell 102 | } 103 | 104 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 105 | tableView.deselectRow(at: indexPath, animated: true) 106 | 107 | if indexPath.section == 0 { 108 | promptForScriptName() 109 | } else { 110 | guard let model = extensions?[indexPath.row] else { return } 111 | let editor = ScriptEditorViewController() 112 | editor.delegate = self 113 | editor.prevModel = model 114 | editor.scriptName = model.name 115 | self.navigationController?.pushViewController(editor, animated: true) 116 | } 117 | } 118 | 119 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 120 | guard editingStyle == .delete else { return } 121 | guard let _ = extensions else { return } 122 | guard let _ = realm else { return } 123 | 124 | do { 125 | try realm.write { 126 | realm.delete(extensions![indexPath.row]) 127 | } 128 | } catch let error as NSError { 129 | print("Could not delete object: \(error.localizedDescription)") 130 | } 131 | } 132 | 133 | // MARK: - Actions 134 | 135 | @objc func promptForScriptName() { 136 | let av = UIAlertController(title: "New Extension", message: "Please provide a name for your extension.", preferredStyle: .alert) 137 | av.addTextField(configurationHandler: { (textField) in 138 | textField.autocapitalizationType = .words 139 | }) 140 | av.addAction(UIAlertAction(title: "Ok", style: .default, handler: { _ in 141 | if let nameText = av.textFields?.first?.text, nameText != "" { 142 | self.presentEditor(name: nameText, source: nil) 143 | } 144 | })) 145 | av.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 146 | 147 | self.present(av, animated: true, completion: nil) 148 | } 149 | 150 | @objc func presentEditor(name: String, source: String?) { 151 | let editor = ScriptEditorViewController() 152 | editor.delegate = self 153 | editor.scriptName = name 154 | editor.importedSource = source 155 | self.navigationController?.pushViewController(editor, animated: (source == nil)) // Animate if no imported source 156 | } 157 | 158 | @objc func addScript(named name: String?, source: String?, injectionTime: Int) { 159 | guard let name = name else { return } 160 | guard let source = source else { return } 161 | 162 | do { 163 | try realm.write { 164 | let id = UUID().uuidString 165 | realm.add(ExtensionModel(value: ["source": source, "name": name, 166 | "id": id, "active": true, 167 | "injectionTime": injectionTime])) 168 | } 169 | } catch let error { 170 | logRealmError(error: error) 171 | } 172 | } 173 | 174 | @objc func toggleScript(sender: UISwitch) { 175 | guard let model = extensions?[sender.tag] else { return } 176 | 177 | do { 178 | try realm.write { 179 | model.active = sender.isOn 180 | } 181 | } catch let error { 182 | logRealmError(error: error) 183 | } 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /RadiumBrowser/FaviconGetter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FaviconGetter.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 2/8/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebKit 11 | 12 | enum FaviconType: Int { 13 | case icon = 0 14 | case apple = 1 15 | case applePrecomposed = 2 16 | case guess = 3 17 | } 18 | 19 | struct Favicon { 20 | var iconURL: String? 21 | var appleURL: String? 22 | var applePrecomposed: String? 23 | var guess: String? 24 | 25 | func getPreferredURL() -> String? { 26 | if let applePrecomposed = applePrecomposed { 27 | return applePrecomposed 28 | } else if let appleURL = appleURL { 29 | return appleURL 30 | } else if let iconURL = iconURL { 31 | return iconURL 32 | } else if let guess = guess { 33 | return guess 34 | } 35 | 36 | return nil 37 | } 38 | } 39 | 40 | class FaviconGetter: BuiltinExtension, WKScriptMessageHandler { 41 | override var extensionName: String { 42 | return "Favicon Getter" 43 | } 44 | 45 | override init(container: WebContainer) { 46 | super.init(container: container) 47 | 48 | scriptHandlerName = "faviconsMessageHandler" 49 | 50 | if let path = Bundle.main.url(forResource: "Favicons", withExtension: "js") { 51 | if let source = try? String(contentsOf: path, encoding: .utf8) { 52 | let userscript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) 53 | webScript = userscript 54 | container.webView?.configuration.userContentController.addUserScript(userscript) 55 | } 56 | } 57 | } 58 | 59 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 60 | guard let icons = message.body as? [String: Int] else { return } 61 | 62 | var favicon = Favicon() 63 | for icon in icons { 64 | guard let type = FaviconType(rawValue: icon.1) else { continue } 65 | switch type { 66 | case .icon: 67 | favicon.iconURL = icon.0 68 | case .apple: 69 | favicon.appleURL = icon.0 70 | case .applePrecomposed: 71 | favicon.applePrecomposed = icon.0 72 | case .guess: 73 | favicon.guess = icon.0 74 | } 75 | } 76 | 77 | webContainer?.favicon = favicon 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /RadiumBrowser/Favicons.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | if (!window.__firefox__) { 6 | window.__firefox__ = {}; 7 | } 8 | 9 | window.__firefox__.favicons = function() { 10 | // These integers should be kept in sync with the IconType raw-values 11 | var ICON = 0; 12 | var APPLE = 1; 13 | var APPLE_PRECOMPOSED = 2; 14 | var GUESS = 3; 15 | 16 | var selectors = { "link[rel~='icon']": ICON, 17 | "link[rel='apple-touch-icon']": APPLE, 18 | "link[rel='apple-touch-icon-precomposed']": APPLE_PRECOMPOSED 19 | }; 20 | 21 | function getAll() { 22 | var favicons = {}; 23 | 24 | for (var selector in selectors) { 25 | var icons = document.head.querySelectorAll(selector); 26 | for (var i = 0; i < icons.length; i++) { 27 | var href = icons[i].href; 28 | favicons[href] = selectors[selector]; 29 | } 30 | } 31 | 32 | // If we didn't find anything in the page, look to see if a favicon.ico file exists for the domain 33 | if (Object.keys(favicons).length === 0) { 34 | var href = document.location.origin + "/favicon.ico"; 35 | favicons[href] = GUESS; 36 | } 37 | return favicons; 38 | } 39 | 40 | function getFavicons() { 41 | var favicons = getAll(); 42 | webkit.messageHandlers.faviconsMessageHandler.postMessage(favicons); 43 | } 44 | 45 | return { 46 | getFavicons : getFavicons 47 | }; 48 | 49 | }(); 50 | 51 | window.__firefox__.favicons.getFavicons(); 52 | -------------------------------------------------------------------------------- /RadiumBrowser/HistoryEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryEntry.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/5/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | class HistoryEntry: Object { 13 | @objc dynamic var id = "" 14 | @objc dynamic var pageURL = "" 15 | @objc dynamic var pageTitle = "" 16 | @objc dynamic var visitDate = Date(timeIntervalSince1970: 1) 17 | 18 | override class func primaryKey() -> String? { 19 | return "id" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RadiumBrowser/HistoryTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryTableViewController.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/6/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | 12 | 13 | protocol HistoryNavigationDelegate: class { 14 | func didSelectEntry(with url: URL?) 15 | } 16 | 17 | class HistoryTableViewController: UITableViewController { 18 | 19 | weak var delegate: HistoryNavigationDelegate? 20 | 21 | @objc var notificationToken: NotificationToken! 22 | var realm: Realm! 23 | 24 | var history: Results? 25 | 26 | deinit { 27 | notificationToken.invalidate() 28 | } 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | title = "History" 34 | 35 | // Uncomment the following line to display an Edit button in the navigation bar for this view controller. 36 | // self.navigationItem.rightBarButtonItem = self.editButtonItem() 37 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(done)) 38 | 39 | do { 40 | realm = try Realm() 41 | history = realm.objects(HistoryEntry.self).sorted(byKeyPath: "visitDate", ascending: false) 42 | notificationToken = history?.observe { [weak self] (changes: RealmCollectionChange) in 43 | guard let tableView = self?.tableView else { return } 44 | switch changes { 45 | case .initial: 46 | tableView.reloadData() 47 | case .update(_, let deletions, let insertions, let modifications): 48 | tableView.beginUpdates() 49 | tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), 50 | with: .automatic) 51 | tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), 52 | with: .automatic) 53 | tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), 54 | with: .automatic) 55 | tableView.endUpdates() 56 | case .error(let error): 57 | logRealmError(error: error) 58 | } 59 | } 60 | } catch let error { 61 | logRealmError(error: error) 62 | } 63 | 64 | self.tableView.reloadData() 65 | } 66 | 67 | override func didReceiveMemoryWarning() { 68 | super.didReceiveMemoryWarning() 69 | // Dispose of any resources that can be recreated. 70 | } 71 | 72 | @objc func done() { 73 | self.dismiss(animated: true, completion: nil) 74 | } 75 | 76 | // MARK: - Table view data source 77 | 78 | override func numberOfSections(in tableView: UITableView) -> Int { 79 | return 1 80 | } 81 | 82 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 83 | return history?.count ?? 0 84 | } 85 | 86 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 87 | let cellIdentifier = "cell" 88 | 89 | var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) 90 | if cell == nil { 91 | cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier) 92 | } 93 | 94 | let entry = history?[indexPath.row] 95 | if let pageTitle = entry?.pageTitle, pageTitle != "" { 96 | cell?.textLabel?.text = pageTitle 97 | 98 | let attrString = getGrayDate(entry!.visitDate) 99 | attrString.append(NSAttributedString(string: entry!.pageURL)) 100 | cell?.detailTextLabel?.attributedText = attrString 101 | } else { 102 | cell?.textLabel?.text = entry?.pageURL 103 | cell?.detailTextLabel?.attributedText = getGrayDate(entry!.visitDate, attachDash: false) 104 | } 105 | 106 | return cell! 107 | } 108 | 109 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 110 | defer { 111 | self.dismiss(animated: true, completion: nil) 112 | } 113 | guard let pageURL = history?[indexPath.row].pageURL else { return } 114 | 115 | let url = URL(string: pageURL) 116 | delegate?.didSelectEntry(with: url) 117 | } 118 | 119 | override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 120 | return true 121 | } 122 | 123 | // Override to support editing the table view. 124 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 125 | guard editingStyle == .delete else { return } 126 | guard let _ = history else { return } 127 | guard let _ = realm else { return } 128 | 129 | do { 130 | try realm.write { 131 | realm.delete(history!.filter("id = %@", history![indexPath.row].id)) 132 | } 133 | } catch let error as NSError { 134 | print("Could not delete object: \(error.localizedDescription)") 135 | } 136 | } 137 | 138 | func getGrayDate(_ date: Date, attachDash: Bool = true) -> NSMutableAttributedString { 139 | let df = DateFormatter() 140 | df.dateFormat = "MM/dd/yy hh:mma" 141 | let dateString = df.string(from: date) 142 | 143 | return NSMutableAttributedString(string: "\(dateString)\((attachDash) ? " - " : "")", attributes: [.foregroundColor: UIColor.gray]) 144 | } 145 | 146 | } 147 | 148 | // Helper function inserted by Swift 4.2 migrator. 149 | fileprivate func convertToOptionalNSAttributedStringKeyDictionary(_ input: [String: Any]?) -> [NSAttributedString.Key: Any]? { 150 | guard let input = input else { return nil } 151 | return Dictionary(uniqueKeysWithValues: input.map { key, value in (NSAttributedString.Key(rawValue: key), value)}) 152 | } 153 | -------------------------------------------------------------------------------- /RadiumBrowser/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeIconFiles 11 | 12 | CFBundleTypeName 13 | JavaScript Document 14 | LSHandlerRank 15 | Alternate 16 | LSItemContentTypes 17 | 18 | com.netscape.javascript-source 19 | 20 | 21 | 22 | CFBundleExecutable 23 | $(EXECUTABLE_NAME) 24 | CFBundleIdentifier 25 | $(PRODUCT_BUNDLE_IDENTIFIER) 26 | CFBundleInfoDictionaryVersion 27 | 6.0 28 | CFBundleName 29 | Radium 30 | CFBundlePackageType 31 | APPL 32 | CFBundleShortVersionString 33 | $(MARKETING_VERSION) 34 | CFBundleURLTypes 35 | 36 | 37 | CFBundleURLSchemes 38 | 39 | fb1438723366248758 40 | 41 | 42 | 43 | CFBundleVersion 44 | $(CURRENT_PROJECT_VERSION) 45 | Fabric 46 | 47 | APIKey 48 | 49 | Kits 50 | 51 | 52 | KitInfo 53 | 54 | KitName 55 | Crashlytics 56 | 57 | 58 | 59 | FacebookAppID 60 | 1438723366248758 61 | FacebookDisplayName 62 | Radium Web Browser 63 | LSRequiresIPhoneOS 64 | 65 | NSAppTransportSecurity 66 | 67 | NSAllowsArbitraryLoads 68 | 69 | NSAllowsArbitraryLoadsInWebContent 70 | 71 | NSExceptionDomains 72 | 73 | localhost 74 | 75 | NSExceptionAllowsInsecureHTTPLoads 76 | 77 | NSIncludesSubdomains 78 | 79 | 80 | 81 | 82 | NSUserActivityTypes 83 | 84 | OpenUrlIntent 85 | 86 | UILaunchStoryboardName 87 | LaunchScreen 88 | UIMainStoryboardFile 89 | Main 90 | UIRequiredDeviceCapabilities 91 | 92 | armv7 93 | 94 | UISupportedInterfaceOrientations 95 | 96 | UIInterfaceOrientationPortrait 97 | UIInterfaceOrientationLandscapeLeft 98 | UIInterfaceOrientationLandscapeRight 99 | 100 | UISupportedInterfaceOrientations~ipad 101 | 102 | UIInterfaceOrientationPortrait 103 | UIInterfaceOrientationPortraitUpsideDown 104 | UIInterfaceOrientationLandscapeLeft 105 | UIInterfaceOrientationLandscapeRight 106 | 107 | UTImportedTypeDeclarations 108 | 109 | 110 | UTTypeConformsTo 111 | 112 | public.source-code 113 | 114 | UTTypeDescription 115 | JavaScript Document 116 | UTTypeIdentifier 117 | com.netscape.javascript-source 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /RadiumBrowser/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 | -------------------------------------------------------------------------------- /RadiumBrowser/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import LUAutocompleteView 11 | 12 | 13 | class MainViewController: UIViewController, HistoryNavigationDelegate { 14 | 15 | @objc var container: UIView? 16 | @objc var tabContainer: TabContainerView? 17 | var addressBar: AddressBar! 18 | private let autocompleteView = LUAutocompleteView() 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | self.view.backgroundColor = Colors.radiumDarkGray 24 | 25 | let padding = UIView().then { [unowned self] in 26 | $0.backgroundColor = Colors.radiumDarkGray 27 | 28 | self.view.addSubview($0) 29 | $0.snp.makeConstraints { (make) in 30 | make.width.equalTo(self.view) 31 | if #available(iOS 11.0, *) { 32 | make.height.equalTo(self.view.safeAreaInsets.top) 33 | } else { 34 | make.height.equalTo(UIApplication.shared.statusBarFrame.height) 35 | } 36 | make.top.equalTo(self.view) 37 | } 38 | } 39 | 40 | tabContainer = TabContainerView(frame: .zero).then { [unowned self] in 41 | $0.addTabButton?.addTarget(self, action: #selector(self.addTab), for: .touchUpInside) 42 | $0.tabCountButton.addTarget(self, action: #selector(showTabTray), for: .touchUpInside) 43 | 44 | self.view.addSubview($0) 45 | $0.snp.makeConstraints { (make) in 46 | if #available(iOS 11.0, *) { 47 | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) 48 | } else { 49 | make.top.equalTo(padding.snp.bottom) 50 | } 51 | make.left.equalTo(self.view) 52 | make.width.equalTo(self.view) 53 | make.height.equalTo(TabContainerView.standardHeight) 54 | } 55 | } 56 | 57 | addressBar = AddressBar(frame: .zero).then { [unowned self] in 58 | $0.tabContainer = self.tabContainer 59 | self.tabContainer?.addressBar = $0 60 | 61 | $0.setupNaviagtionActions(forTabConatiner: self.tabContainer!) 62 | $0.menuButton?.addTarget(self, action: #selector(self.showMenu(sender:)), for: .touchUpInside) 63 | 64 | self.view.addSubview($0) 65 | $0.snp.makeConstraints { (make) in 66 | make.top.equalTo(self.tabContainer!.snp.bottom) 67 | make.left.width.equalTo(self.view) 68 | make.height.equalTo(AddressBar.standardHeight) 69 | } 70 | } 71 | 72 | container = UIView().then { [unowned self] in 73 | self.tabContainer?.containerView = $0 74 | 75 | self.view.addSubview($0) 76 | $0.snp.makeConstraints { (make) in 77 | make.top.equalTo(addressBar.snp.bottom) 78 | make.width.equalTo(self.view) 79 | make.bottom.equalTo(self.view) 80 | make.left.equalTo(self.view) 81 | } 82 | } 83 | 84 | self.view.addSubview(autocompleteView) 85 | autocompleteView.textField = addressBar.addressField 86 | autocompleteView.dataSource = self 87 | autocompleteView.delegate = self 88 | autocompleteView.rowHeight = 45 89 | autocompleteView.autocompleteCell = AutocompleteTableViewCell.self 90 | autocompleteView.throttleTime = 0.2 91 | 92 | tabContainer?.loadBrowsingSession() 93 | 94 | if UserDefaults.standard.bool(forKey: SettingsKeys.needToShowAdBlockAlert) { 95 | showAdBlockEnabled() 96 | } 97 | 98 | addressBar.addressField?.becomeFirstResponder() 99 | } 100 | 101 | override func didReceiveMemoryWarning() { 102 | super.didReceiveMemoryWarning() 103 | // Dispose of any resources that can be recreated. 104 | } 105 | 106 | override func viewWillDisappear(_ animated: Bool) { 107 | super.viewWillDisappear(animated) 108 | 109 | tabContainer?.currentTab?.webContainer?.takeScreenshot() 110 | } 111 | 112 | override func viewDidLayoutSubviews() { 113 | tabContainer?.setUpTabConstraints() 114 | } 115 | 116 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 117 | tabContainer?.setUpTabConstraints() 118 | } 119 | 120 | override var prefersHomeIndicatorAutoHidden: Bool { 121 | return true 122 | } 123 | 124 | func showAdBlockEnabled() { 125 | UserDefaults.standard.set(false, forKey: SettingsKeys.needToShowAdBlockAlert) 126 | 127 | let av = UIAlertController(title: "Ad Block Enabled!", message: "Thank you for being an early adopter of Radium! As a token of my grattitude you have received the new Ad Block add on free of charge! This will block ads from known sources on web pages you visit. Happy browsing!", preferredStyle: .alert) 128 | av.addAction(UIAlertAction(title: "Settings", style: .default, handler: { _ in 129 | self.showSettings() 130 | })) 131 | av.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) 132 | 133 | delay(0.5) { 134 | self.present(av, animated: true, completion: nil) 135 | } 136 | } 137 | 138 | @objc func addTab() { 139 | let _ = tabContainer?.addNewTab(container: container!) 140 | } 141 | 142 | @objc func showMenu(sender: UIButton) { 143 | let convertedPoint = sender.convert(sender.center, to: self.view) 144 | 145 | let addBookmarkAction = MenuItem.item(named: "Add Bookmark", action: { [unowned self] in 146 | self.addBookmark(btn: sender) 147 | }) 148 | let bookmarkAction = MenuItem.item(named: "Bookmarks", action: { [unowned self] in 149 | self.showBookmarks() 150 | }) 151 | let shareAction = MenuItem.item(named: "Share", action: { [unowned self] in 152 | self.shareLink() 153 | }) 154 | let extensionAction = MenuItem.item(named: "Extensions", action: { [unowned self] in 155 | let _ = self.showExtensions(animated: true) 156 | }) 157 | let historyAction = MenuItem.item(named: "History", action: { [unowned self] in 158 | self.showHistory() 159 | }) 160 | let settingsAction = MenuItem.item(named: "Settings", action: { [unowned self] in 161 | self.showSettings() 162 | }) 163 | 164 | let menu = SharedDropdownMenu(menuItems: [addBookmarkAction, bookmarkAction, shareAction, extensionAction, historyAction, settingsAction]) 165 | menu.show(in: self.view, from: convertedPoint) 166 | } 167 | 168 | @objc func shareLink() { 169 | guard let tabContainer = self.tabContainer else { return } 170 | let selectedTab = tabContainer.tabList[tabContainer.selectedTabIndex] 171 | 172 | guard let url = selectedTab.webContainer?.webView?.url else { return } 173 | let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil) 174 | activityVC.excludedActivityTypes = [.print] 175 | activityVC.completionWithItemsHandler = { _, completed, _, _ in 176 | if completed { 177 | } 178 | } 179 | self.present(activityVC, animated: true, completion: nil) 180 | } 181 | 182 | @objc func showExtensions(animated: Bool) -> ExtensionsTableViewController { 183 | let vc = ExtensionsTableViewController(style: .grouped) 184 | let nav = UINavigationController(rootViewController: vc) 185 | nav.navigationBar.barTintColor = Colors.radiumGray 186 | 187 | if isiPadUI { 188 | nav.modalPresentationStyle = .formSheet 189 | } 190 | 191 | self.present(nav, animated: animated, completion: nil) 192 | 193 | return vc 194 | } 195 | 196 | @objc func showHistory() { 197 | let vc = HistoryTableViewController() 198 | vc.delegate = self 199 | let nav = UINavigationController(rootViewController: vc) 200 | nav.navigationBar.barTintColor = Colors.radiumGray 201 | 202 | if isiPadUI { 203 | nav.modalPresentationStyle = .formSheet 204 | } 205 | 206 | self.present(nav, animated: true, completion: nil) 207 | } 208 | 209 | @objc func showBookmarks() { 210 | let flowLayout = UICollectionViewFlowLayout() 211 | flowLayout.itemSize = CGSize(width: 80, height: 97.5) 212 | let vc = BookmarkCollectionViewController(collectionViewLayout: flowLayout) 213 | vc.delegate = self 214 | let nav = UINavigationController(rootViewController: vc) 215 | nav.navigationBar.barTintColor = Colors.radiumGray 216 | 217 | if isiPadUI { 218 | nav.modalPresentationStyle = .formSheet 219 | } 220 | 221 | self.present(nav, animated: true, completion: nil) 222 | } 223 | 224 | @objc func addBookmark(btn: UIView) { 225 | let vc = AddBookmarkTableViewController(style: .grouped) 226 | vc.pageIconURL = tabContainer?.currentTab?.webContainer?.favicon?.getPreferredURL() 227 | vc.pageTitle = tabContainer?.currentTab?.webContainer?.webView?.title 228 | vc.pageURL = tabContainer?.currentTab?.webContainer?.webView?.url?.absoluteString 229 | let nav = UINavigationController(rootViewController: vc) 230 | 231 | if isiPadUI { 232 | nav.modalPresentationStyle = .popover 233 | nav.popoverPresentationController?.permittedArrowDirections = .up 234 | nav.popoverPresentationController?.sourceView = btn 235 | nav.popoverPresentationController?.sourceRect = btn.bounds 236 | } 237 | 238 | self.present(nav, animated: true, completion: nil) 239 | } 240 | 241 | @objc func didSelectEntry(with url: URL?) { 242 | guard let url = url else { return } 243 | tabContainer?.loadQuery(string: url.absoluteString) 244 | } 245 | 246 | func showSettings() { 247 | let vc = SettingsTableViewController(style: .grouped) 248 | let nav = UINavigationController(rootViewController: vc) 249 | 250 | if isiPadUI { 251 | nav.modalPresentationStyle = .formSheet 252 | } 253 | 254 | self.present(nav, animated: true, completion: nil) 255 | } 256 | 257 | @objc func showTabTray() { 258 | let vc = TabTrayViewController() 259 | 260 | self.present(vc, animated: true, completion: nil) 261 | } 262 | 263 | // MARK: - Import methods 264 | 265 | @objc func openEditor(withSource source: String, andName name: String) { 266 | if let presentedController = self.presentedViewController { 267 | presentedController.dismiss(animated: false, completion: nil) 268 | } 269 | 270 | let vc = self.showExtensions(animated: false) 271 | delay(0.15) { 272 | vc.presentEditor(name: name, source: source) 273 | } 274 | } 275 | } 276 | 277 | extension MainViewController: LUAutocompleteViewDataSource { 278 | func autocompleteView(_ autocompleteView: LUAutocompleteView, elementsFor text: String, completion: @escaping ([String]) -> Void) { 279 | let results = SuggestionManager.shared.queryDomains(forText: text).map { $0.urlString } 280 | completion(results) 281 | } 282 | } 283 | 284 | extension MainViewController: LUAutocompleteViewDelegate { 285 | func autocompleteView(_ autocompleteView: LUAutocompleteView, didSelect text: String) { 286 | addressBar.addressField?.text = text 287 | _ = addressBar.textFieldShouldReturn(addressBar.addressField!) 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /RadiumBrowser/MigrationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigrationManager.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/5/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | import SwiftKeychainWrapper 12 | 13 | class MigrationManager: NSObject { 14 | @objc static let shared = MigrationManager() 15 | 16 | @objc func attemptMigration() { 17 | let realmConfig = Realm.Configuration( 18 | schemaVersion: 6, 19 | migrationBlock: { migration, oldSchemaVersion in 20 | if oldSchemaVersion < 1 { 21 | migration.enumerateObjects(ofType: ExtensionModel.className()) { _, newObject in 22 | newObject?["active"] = true 23 | } 24 | } 25 | 26 | if oldSchemaVersion < 2 { 27 | migration.enumerateObjects(ofType: BrowsingSession.className()) { _, newObject in 28 | newObject?["selectedTabIndex"] = 0 29 | } 30 | } 31 | 32 | if oldSchemaVersion < 3 { 33 | migration.enumerateObjects(ofType: ExtensionModel.className()) { _, newObject in 34 | newObject?["injectionTime"] = 1 35 | } 36 | } 37 | 38 | if oldSchemaVersion < 4 { 39 | migration.enumerateObjects(ofType: BrowsingSession.className()) { _, newObject in 40 | newObject?["selectedTabIndex"] = 0 41 | } 42 | } 43 | 44 | if oldSchemaVersion < 5 { 45 | migration.enumerateObjects(ofType: Bookmark.className()) { _, newObject in 46 | newObject?["iconURL"] = "" 47 | } 48 | } 49 | 50 | // Using this to grand father current users in for ad blocking 51 | if #available(iOS 11.0, *), oldSchemaVersion < 6 { 52 | KeychainWrapper.standard.set(true, forKey: SettingsKeys.adBlockPurchased) 53 | UserDefaults.standard.set(true, forKey: SettingsKeys.needToShowAdBlockAlert) 54 | UserDefaults.standard.set(true, forKey: SettingsKeys.adBlockEnabled) 55 | } 56 | } 57 | ) 58 | 59 | Realm.Configuration.defaultConfiguration = realmConfig 60 | 61 | if !UserDefaults.standard.bool(forKey: "cloudToButtAdded") { 62 | if let filePath = Bundle.main.path(forResource: "CloudToButt", ofType: "js"), 63 | let content = try? String(contentsOfFile: filePath, encoding: .utf8) { 64 | UserDefaults.standard.set(true, forKey: "cloudToButtAdded") 65 | 66 | let exten = ExtensionModel() 67 | exten.id = UUID().uuidString 68 | exten.name = "Cloud To Butt" 69 | exten.source = content 70 | exten.active = false 71 | 72 | do { 73 | let realm = try Realm() 74 | try realm.write { 75 | realm.add(exten) 76 | } 77 | } catch { 78 | print("Realm error: \(error.localizedDescription)") 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /RadiumBrowser/NotificationExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotifactionExtensions.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 11/6/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSNotification.Name { 12 | static let adBlockSettingsChanged = Notification.Name("adBlockSettingsChanged") 13 | } 14 | -------------------------------------------------------------------------------- /RadiumBrowser/RadiumBrowser-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 | #import 6 | #import 7 | #import 8 | -------------------------------------------------------------------------------- /RadiumBrowser/ScriptEditorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptEditorViewController.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/2/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | import Highlightr 12 | 13 | 14 | protocol ScriptEditorDelegate: class { 15 | func addScript(named name: String?, source: String?, injectionTime: Int) 16 | } 17 | 18 | class ScriptEditorViewController: UIViewController, UITextViewDelegate { 19 | 20 | @objc var textView: UITextView? 21 | @objc var injectionTimeSelector: UISegmentedControl? 22 | @objc var scriptName: String? 23 | @objc var importedSource: String? 24 | 25 | @objc var prevModel: ExtensionModel? 26 | 27 | weak var delegate: ScriptEditorDelegate? 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | self.navigationItem.prompt = scriptName 33 | 34 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .done, target: self, action: #selector(self.done(sender:))) 35 | 36 | let textStorage = CodeAttributedString() 37 | textStorage.language = "JavaScript" 38 | textStorage.highlightr.setTheme(to: "monokai-sublime") 39 | let layoutManager = NSLayoutManager() 40 | textStorage.addLayoutManager(layoutManager) 41 | 42 | let textContainer = NSTextContainer(size: view.bounds.size) 43 | layoutManager.addTextContainer(textContainer) 44 | 45 | textView = UITextView(frame: .zero, textContainer: textContainer).then { [unowned self] in 46 | $0.autocorrectionType = .no 47 | $0.autocapitalizationType = .none 48 | $0.font = UIFont(name: "Menlo-Regular", size: UIFont.systemFontSize + 3) 49 | $0.inputAccessoryView = DoneAccessoryView(targetView: $0, width: self.view.frame.width) 50 | $0.backgroundColor = textStorage.highlightr.theme.themeBackgroundColor 51 | $0.keyboardType = .asciiCapable 52 | $0.delegate = self 53 | 54 | if let importedSource = self.importedSource { 55 | $0.attributedText = textStorage.highlightr.highlight(importedSource) 56 | } else if let prevModel = self.prevModel { 57 | $0.text = prevModel.source 58 | } 59 | 60 | self.view.addSubview($0) 61 | $0.snp.makeConstraints { (make) in 62 | make.edges.equalTo(self.view) 63 | } 64 | } 65 | 66 | injectionTimeSelector = UISegmentedControl(items: ["At Start", "At End"]).then { [unowned self] in 67 | $0.sizeToFit() 68 | 69 | if let prevModel = self.prevModel { 70 | $0.selectedSegmentIndex = prevModel.injectionTime 71 | } else { 72 | $0.selectedSegmentIndex = 1 73 | } 74 | self.navigationItem.titleView = $0 75 | } 76 | 77 | if let _ = importedSource { 78 | // If we are importing a file, allow the user to select a new name 79 | promptForNewName() 80 | } 81 | } 82 | 83 | override func viewWillAppear(_ animated: Bool) { 84 | super.viewWillAppear(animated) 85 | 86 | let notificationCenter = NotificationCenter.default 87 | notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(notification:)), 88 | name: UIResponder.keyboardWillShowNotification, object: nil) 89 | notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(notification:)), 90 | name: UIResponder.keyboardWillHideNotification, object: nil) 91 | } 92 | 93 | override func viewWillDisappear(_ animated: Bool) { 94 | super.viewWillDisappear(animated) 95 | 96 | let notificationCenter = NotificationCenter.default 97 | notificationCenter.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) 98 | notificationCenter.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) 99 | } 100 | 101 | override func didReceiveMemoryWarning() { 102 | super.didReceiveMemoryWarning() 103 | // Dispose of any resources that can be recreated. 104 | } 105 | 106 | // MARK: - Helpers 107 | 108 | @objc func promptForNewName() { 109 | let av = UIAlertController(title: "New Extension", message: "Please provide a name for your extension.", preferredStyle: .alert) 110 | av.addTextField(configurationHandler: { (textField) in 111 | textField.autocapitalizationType = .words 112 | textField.text = self.scriptName 113 | }) 114 | av.addAction(UIAlertAction(title: "Ok", style: .default, handler: { _ in 115 | if let nameText = av.textFields?.first?.text, nameText != "" { 116 | self.scriptName = nameText 117 | self.navigationItem.prompt = nameText 118 | } 119 | })) 120 | av.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 121 | 122 | self.present(av, animated: true, completion: nil) 123 | } 124 | 125 | // MARK: - Saving 126 | 127 | @objc func done(sender: UIBarButtonItem) { 128 | saveChanges() 129 | let _ = self.navigationController?.popViewController(animated: true) 130 | } 131 | 132 | @objc func saveChanges() { 133 | if let model = prevModel { 134 | do { 135 | let realm = try Realm() 136 | try realm.write { 137 | model.source = textView!.text 138 | model.injectionTime = injectionTimeSelector!.selectedSegmentIndex 139 | } 140 | } catch let error { 141 | logRealmError(error: error) 142 | } 143 | } else { 144 | delegate?.addScript(named: scriptName, source: textView?.text, injectionTime: injectionTimeSelector!.selectedSegmentIndex) 145 | } 146 | } 147 | 148 | // MARK: - Keyboard Methods 149 | 150 | @objc func getTextViewInsets(keyboardHeight: CGFloat) -> CGFloat { 151 | // Calculate the offset of our tableView in the 152 | // coordinate space of of our window 153 | guard let window = (UIApplication.shared.delegate as? AppDelegate)?.window else { return 0 } 154 | let tableViewFrame = textView!.superview!.convert(textView!.frame, to: window) 155 | 156 | // BottomInset = part of keyboard that is covering the tableView 157 | let bottomInset = keyboardHeight - (window.frame.height - tableViewFrame.height - tableViewFrame.origin.y) 158 | 159 | // Return the new insets + update this if you have custom insets 160 | return bottomInset - 161 | CGFloat((UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeLeft) ? 162 | 44 : 163 | 0) 164 | } 165 | 166 | @objc func keyboardWillShow(notification: NSNotification) { 167 | let userInfo = notification.userInfo! 168 | guard let keyboardHeight = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height else { return } 169 | 170 | if isiPadUI { 171 | textView?.snp.remakeConstraints { (make) in 172 | make.edges.equalTo(self.view).inset(UIEdgeInsets(top: 0, left: 0, 173 | bottom: getTextViewInsets(keyboardHeight: keyboardHeight), 174 | right: 0)) 175 | } 176 | } else { 177 | textView?.snp.remakeConstraints { (make) in 178 | make.edges.equalTo(self.view).inset(UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)) 179 | } 180 | } 181 | 182 | UIView.animate(withDuration: 0.3) { 183 | self.view.layoutIfNeeded() 184 | } 185 | } 186 | 187 | @objc func keyboardWillHide(notification: NSNotification) { 188 | textView?.snp.remakeConstraints { (make) in 189 | make.edges.equalTo(self.view) 190 | } 191 | 192 | UIView.animate(withDuration: 0.3) { 193 | self.view.layoutIfNeeded() 194 | } 195 | } 196 | 197 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { 198 | guard text == "\n" else { return true } 199 | 200 | let end = textView.text.index(textView.text.startIndex, offsetBy: range.location) 201 | guard let prevLine = String(textView.text[.. 0 { 213 | postString = "\n" + ((" " * tabSize) * (indentationLevel - nextLevel)) + "}" 214 | } 215 | 216 | if range.location == textView.text.count { 217 | let updatedText = paddingString 218 | textView.text = textView.text + updatedText + postString 219 | } else { 220 | let beginning = textView.beginningOfDocument 221 | let start = textView.position(from: beginning, offset: range.location) 222 | let rangeEnd = textView.position(from: start!, offset: range.length) 223 | let textRange = textView.textRange(from: start!, to: rangeEnd!) 224 | 225 | textView.replace(textRange!, withText: paddingString + postString) 226 | } 227 | 228 | let cursor = NSRange(location: range.location + paddingString.count, length: 0) 229 | textView.selectedRange = cursor 230 | 231 | return false 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /RadiumBrowser/SharedConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedConfig.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import BSColorUtils 12 | 13 | struct Colors { 14 | static let radiumGray = UIColor.with(hex: "#EFEFEF") 15 | static let radiumDarkGray = UIColor.with(hex: "#AFB4B4") 16 | static let radiumUnselected = UIColor.with(hex: "#C8C8C8") 17 | static let urlGreen = UIColor.with(hex: "#046D23") 18 | } 19 | 20 | struct SettingsKeys { 21 | static let firstRun = "firstRun" 22 | static let trackHistory = "trackHistory" 23 | static let adBlockEnabled = "adBlockEnabled" 24 | static let stringLiteralAdBlock = "stringLiteralAdBlock" 25 | static let adBlockPurchased = "purchasedAdBlock" 26 | static let needToShowAdBlockAlert = "needToShowAdBlockAlert" 27 | static let searchEngineUrl = "searchEngineUrl" 28 | } 29 | 30 | enum HostFileNames: String { 31 | case adaway 32 | case blackHosts 33 | case malwareHosts 34 | case camelon 35 | case zeus 36 | case tracker 37 | case simpleAds 38 | case adServerHosts 39 | case ultimateAdBlock 40 | 41 | static let allValues: [HostFileNames] = [.adaway, .blackHosts, .malwareHosts, .camelon, .zeus, .tracker, .simpleAds, .adServerHosts, .ultimateAdBlock] 42 | } 43 | 44 | let isiPadUI = UI_USER_INTERFACE_IDIOM() == .pad 45 | let isiPhone5 = UIScreen.main.bounds.height == 568 46 | 47 | func logRealmError(error: Error) { 48 | print("## Realm Error: \(error.localizedDescription)") 49 | } 50 | 51 | func delay(_ delay: Double, closure: @escaping ()->()) { 52 | DispatchQueue.main.asyncAfter( 53 | deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) 54 | } 55 | -------------------------------------------------------------------------------- /RadiumBrowser/SharedDropdownMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedDropdownMenu.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/1/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct MenuItem { 12 | var name: String? 13 | var action: (() -> ())? 14 | 15 | static func item(named name: String, action: (() -> ())?) -> MenuItem { 16 | var menuItem = MenuItem() 17 | menuItem.name = " " + name 18 | menuItem.action = action 19 | return menuItem 20 | } 21 | } 22 | 23 | class SharedDropdownMenu: UIView, UIGestureRecognizerDelegate { 24 | 25 | @objc static let defaultMenuItemHeight: CGFloat = 44 26 | @objc static let defaultMenuWidth: CGFloat = 250 27 | 28 | var menuItems: [MenuItem]? 29 | 30 | @objc var dismissGesture: UITapGestureRecognizer? 31 | 32 | var displayOrigin: CGPoint? 33 | 34 | init(menuItems: [MenuItem]) { 35 | super.init(frame: .zero) 36 | 37 | self.layer.cornerRadius = 4 38 | self.layer.borderColor = UIColor.gray.cgColor 39 | self.layer.borderWidth = 0.25 40 | self.clipsToBounds = true 41 | 42 | self.menuItems = menuItems 43 | setupMenu() 44 | } 45 | 46 | required init?(coder aDecoder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | @objc func setupMenu() { 51 | var itemViews = [UIView]() 52 | for (i, menuItem) in menuItems!.enumerated() { 53 | let _ = UIButton().then { [unowned self] in 54 | $0.setTitle(menuItem.name, for: .normal) 55 | $0.setTitleColor(.black, for: .normal) 56 | $0.setTitleColor(.lightGray, for: .highlighted) 57 | $0.contentHorizontalAlignment = .left 58 | $0.backgroundColor = Colors.radiumGray 59 | $0.layer.borderWidth = 0.5 60 | $0.layer.borderColor = UIColor.gray.cgColor 61 | $0.tag = i 62 | 63 | $0.addTarget(self, action: #selector(self.tappedItem(sender:)), for: .touchUpInside) 64 | 65 | self.addSubview($0) 66 | $0.snp.makeConstraints { (make) in 67 | if i == 0 { 68 | make.top.equalTo(self) 69 | } else { 70 | make.top.equalTo(itemViews[i - 1].snp.bottom) 71 | } 72 | make.left.equalTo(self) 73 | make.width.equalTo(self) 74 | make.height.equalTo(SharedDropdownMenu.defaultMenuItemHeight) 75 | } 76 | 77 | itemViews.append($0) 78 | } 79 | } 80 | } 81 | 82 | @objc func show(in view: UIView, from point: CGPoint) { 83 | let bounds = view.bounds 84 | let height = SharedDropdownMenu.defaultMenuItemHeight * CGFloat(menuItems!.count) 85 | let width = SharedDropdownMenu.defaultMenuWidth 86 | displayOrigin = point 87 | 88 | var x = point.x - (width / 2) 89 | if x + width > bounds.width - 8 { 90 | x = bounds.width - width - 8 91 | } 92 | if x < 8 { 93 | x = 8 94 | } 95 | 96 | let finalFrame = CGRect(x: x, y: point.y, width: width, height: height) 97 | self.frame = CGRect(origin: point, size: .zero) 98 | view.addSubview(self) 99 | 100 | dismissGesture = UITapGestureRecognizer() 101 | dismissGesture?.delegate = self 102 | dismissGesture?.numberOfTapsRequired = 1 103 | view.window?.addGestureRecognizer(dismissGesture!) 104 | 105 | UIView.animate(withDuration: 0.25) { 106 | self.frame = finalFrame 107 | } 108 | } 109 | 110 | @objc func dismiss() { 111 | if let _ = dismissGesture { 112 | self.window?.removeGestureRecognizer(dismissGesture!) 113 | } 114 | 115 | guard let displayOrigin = displayOrigin else { 116 | self.removeFromSuperview() 117 | return 118 | } 119 | 120 | UIView.animate(withDuration: 0.3, animations: { 121 | self.frame = CGRect(origin: displayOrigin, size: .zero) 122 | }) { _ in 123 | self.removeFromSuperview() 124 | } 125 | } 126 | 127 | @objc func tappedItem(sender: UIButton) { 128 | dismiss() 129 | guard let item = menuItems?[sender.tag] else { return } 130 | item.action?() 131 | } 132 | 133 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 134 | let point = touch.location(in: nil) 135 | if !self.frame.contains(point) { 136 | self.dismiss() 137 | return false 138 | } 139 | return false 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /RadiumBrowser/SharedTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedTextField.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | class SharedTextField: UITextField { 13 | 14 | @IBInspectable var inset: CGFloat = 0 15 | 16 | override func textRect(forBounds bounds: CGRect) -> CGRect { 17 | return bounds.insetBy(dx: inset, dy: inset / 3) 18 | } 19 | 20 | override func editingRect(forBounds bounds: CGRect) -> CGRect { 21 | return textRect(forBounds: bounds) 22 | } 23 | 24 | override func rightViewRect(forBounds bounds: CGRect) -> CGRect { 25 | var rect = super.rightViewRect(forBounds: bounds) 26 | rect.origin.x -= 7 27 | return rect 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /RadiumBrowser/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtensions.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Duplicate a string n number of times. 12 | /// 13 | /// - Parameters: 14 | /// - lhs: The string to duplicate 15 | /// - rhs: The number of times to duplicate the string 16 | /// - Returns: The resulting string 17 | func * (lhs: String, rhs: Int) -> String { 18 | guard rhs > 0 else { return "" } 19 | 20 | var result = "" 21 | for _ in 0.. Bool { 30 | if self.hasPrefix("https://") || self.hasPrefix("http://") { 31 | return true 32 | } 33 | 34 | return self.range(of: "^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$", options: .regularExpression) != nil 35 | } 36 | 37 | var lines: [String] { 38 | var result: [String] = [] 39 | enumerateLines { line, _ in result.append(line) } 40 | return result 41 | } 42 | 43 | func countLeadingSpaces() -> Int { 44 | guard !isEmpty else { return 0 } 45 | 46 | var i = startIndex 47 | var count = 0 48 | while i < endIndex { 49 | let c = self[i] 50 | if c == " " { 51 | count += 1 52 | } else { 53 | break 54 | } 55 | i = self.index(i, offsetBy: 1) 56 | } 57 | 58 | return count 59 | } 60 | 61 | func getIndentationLevel(tabSize: Int) -> Int { 62 | guard tabSize > 0 else { return 0 } 63 | 64 | let formattedString = self.replacingOccurrences(of: "\t", with: " " * tabSize) 65 | 66 | return formattedString.countLeadingSpaces() / tabSize 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /RadiumBrowser/SuggestionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SuggestionManager.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 11/1/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | struct URLEntry: Hashable { 13 | var hashValue: Int { 14 | return urlString.hashValue 15 | } 16 | 17 | static func ==(lhs: URLEntry, rhs: URLEntry) -> Bool { 18 | return lhs.urlString == rhs.urlString 19 | } 20 | 21 | var urlString: String 22 | var titleString: String? 23 | } 24 | 25 | class SuggestionManager { 26 | static let shared = SuggestionManager() 27 | 28 | lazy var domainSet = OrderedSet() 29 | 30 | var topdomains: [URLEntry]? 31 | var historyResults: Results? 32 | 33 | @objc var notificationToken: NotificationToken! 34 | var realm: Realm! 35 | 36 | deinit { 37 | notificationToken.invalidate() 38 | } 39 | 40 | init() { 41 | defer { 42 | reupdateList() 43 | } 44 | 45 | guard let path = Bundle.main.path(forResource: "topdomains", ofType: "txt") else { 46 | return 47 | } 48 | 49 | do { 50 | self.realm = try Realm() 51 | self.historyResults = realm.objects(HistoryEntry.self) 52 | self.notificationToken = historyResults?.observe { [weak self] _ in 53 | self?.reupdateList() 54 | } 55 | } catch let error as NSError { 56 | print("Error occured opening realm: \(error.localizedDescription)") 57 | } 58 | 59 | do { 60 | let domainConent = try String(contentsOfFile: path, encoding: .utf8) 61 | let domainList = domainConent.components(separatedBy: "\n") 62 | 63 | topdomains = domainList.map { URLEntry(urlString: $0, titleString: nil) } 64 | } catch { 65 | return 66 | } 67 | 68 | } 69 | 70 | func reupdateList() { 71 | domainSet.removeAllObjects() 72 | 73 | domainSet.append(contentsOf: topdomains!) 74 | historyResults?.forEach { domainSet.append(URLEntry(urlString: $0.pageURL, titleString: $0.pageTitle)) } 75 | } 76 | 77 | func queryDomains(forText text: String) -> [URLEntry] { 78 | var queryText = text.replacingOccurrences(of: "http://", with: "") 79 | queryText = text.replacingOccurrences(of: "https://", with: "") 80 | 81 | let results: [URLEntry] = domainSet.filter { $0.urlString.contains(queryText) } 82 | 83 | return results 84 | } 85 | 86 | func pageTitle(forURLSring urlString: String) -> String? { 87 | for entry in domainSet where entry.urlString == urlString { 88 | return entry.titleString 89 | } 90 | 91 | return nil 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /RadiumBrowser/TabCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabCollectionViewCell.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 11/7/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol TabTrayCellDelegate: class { 12 | func didTapCloseBtn(tabCell: TabCollectionViewCell, tag: Int) 13 | } 14 | 15 | class TabCollectionViewCell: UICollectionViewCell { 16 | var screenshotView: UIImageView! 17 | var faviconView: UIImageView! 18 | var pageTitle: UILabel! 19 | 20 | var closeTabButton: UIButton! 21 | 22 | weak var delegate: TabTrayCellDelegate? 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | 27 | screenshotView = UIImageView().then { 28 | $0.contentMode = .scaleAspectFill 29 | $0.clipsToBounds = true 30 | 31 | self.contentView.addSubview($0) 32 | $0.snp.makeConstraints { make in 33 | make.edges.equalTo(self.contentView) 34 | } 35 | } 36 | 37 | faviconView = UIImageView().then { 38 | $0.image = UIImage(named: "globe") 39 | $0.contentMode = .scaleAspectFit 40 | 41 | self.contentView.addSubview($0) 42 | $0.snp.makeConstraints { (make) in 43 | make.width.equalTo(15) 44 | make.height.equalTo(15) 45 | make.left.equalTo(self.contentView).offset(8) 46 | make.bottom.equalTo(self.contentView).offset(-8) 47 | } 48 | } 49 | 50 | pageTitle = UILabel().then { 51 | self.contentView.addSubview($0) 52 | $0.snp.makeConstraints { make in 53 | make.left.equalTo(faviconView.snp.right).offset(5) 54 | make.centerY.equalTo(faviconView) 55 | make.right.equalTo(self.contentView).offset(-8) 56 | } 57 | } 58 | 59 | closeTabButton = UIButton(type: .custom).then { 60 | $0.setTitle("Close", for: .normal) 61 | $0.setTitleColor(.white, for: .normal) 62 | $0.backgroundColor = UIColor.gray.withAlphaComponent(0.8) 63 | $0.layer.cornerRadius = 5 64 | $0.addTarget(self, action: #selector(tappedClose(sender:)), for: .touchUpInside) 65 | 66 | self.contentView.addSubview($0) 67 | $0.snp.makeConstraints { make in 68 | make.height.equalTo(25) 69 | make.top.right.equalTo(self.contentView).inset(8) 70 | } 71 | } 72 | } 73 | 74 | required init?(coder aDecoder: NSCoder) { 75 | fatalError("init(coder:) has not been implemented") 76 | } 77 | 78 | @objc func tappedClose(sender: UIButton) { 79 | delegate?.didTapCloseBtn(tabCell: self, tag: sender.tag) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /RadiumBrowser/TabContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabContainerView.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | import WebKit 12 | 13 | class TabContainerView: UIView, TabViewDelegate { 14 | 15 | static var currentInstance: TabContainerView? 16 | 17 | @objc static let standardHeight: CGFloat = 35 18 | 19 | @objc static let defaultTabWidth: CGFloat = 150 20 | @objc static let defaultTabHeight: CGFloat = TabContainerView.standardHeight - 2 21 | 22 | @objc weak var containerView: UIView? 23 | 24 | var tabCountButton: TabCountButton! 25 | var tabScrollView: UIScrollView! 26 | @objc lazy var tabList: [TabView] = [] 27 | 28 | @objc var addTabButton: UIButton? 29 | 30 | @objc var selectedTabIndex = 0 { 31 | didSet { 32 | setTabColors() 33 | } 34 | } 35 | 36 | @objc var currentTab: TabView? { 37 | guard tabList.count > 0 else { return nil } 38 | return tabList[selectedTabIndex] 39 | } 40 | 41 | @objc weak var addressBar: AddressBar? 42 | 43 | override init(frame: CGRect) { 44 | super.init(frame: frame) 45 | 46 | self.backgroundColor = Colors.radiumDarkGray 47 | 48 | addTabButton = UIButton().then { [unowned self] in 49 | $0.setImage(UIImage.imageFrom(systemItem: .add), for: .normal) 50 | 51 | self.addSubview($0) 52 | $0.snp.makeConstraints { (make) in 53 | make.height.equalTo(TabContainerView.standardHeight - 5) 54 | make.width.equalTo(TabContainerView.standardHeight - 5) 55 | make.centerY.equalTo(self) 56 | if #available(iOS 11.0, *) { 57 | make.right.equalTo(self.safeAreaLayoutGuide.snp.right).offset(-8) 58 | } else { 59 | make.right.equalTo(self) 60 | } 61 | } 62 | } 63 | 64 | tabCountButton = TabCountButton().then { 65 | self.addSubview($0) 66 | $0.snp.makeConstraints { make in 67 | make.height.equalTo(TabContainerView.standardHeight - 5) 68 | make.width.equalTo(TabContainerView.standardHeight - 5) 69 | make.centerY.equalTo(self) 70 | if #available(iOS 11.0, *) { 71 | make.left.equalTo(self.safeAreaLayoutGuide.snp.left).offset(8) 72 | } else { 73 | make.left.equalTo(self).offset(4) 74 | } 75 | } 76 | } 77 | 78 | tabScrollView = UIScrollView().then { 79 | $0.isScrollEnabled = true 80 | $0.showsVerticalScrollIndicator = false 81 | $0.showsHorizontalScrollIndicator = false 82 | 83 | self.addSubview($0) 84 | $0.snp.makeConstraints { make in 85 | make.bottom.equalToSuperview() 86 | make.height.equalTo(TabContainerView.defaultTabHeight) 87 | make.left.equalTo(tabCountButton.snp.right).offset(5) 88 | make.right.equalTo(addTabButton!.snp.left).offset(-5) 89 | } 90 | } 91 | 92 | TabContainerView.currentInstance = self 93 | } 94 | 95 | required init?(coder aDecoder: NSCoder) { 96 | fatalError("init(coder:) has not been implemented") 97 | } 98 | 99 | // MARK: - Tab Management 100 | 101 | @objc func addNewTab(container: UIView, focusAddressBar: Bool = true) -> TabView { 102 | let newTab = TabView(parentView: container).then { [unowned self] in 103 | $0.delegate = self 104 | 105 | self.tabScrollView.addSubview($0) 106 | self.tabScrollView.sendSubviewToBack($0) 107 | } 108 | tabList.append(newTab) 109 | 110 | setUpTabConstraints() 111 | didTap(tab: newTab) 112 | if focusAddressBar && tabList.count > 1 { 113 | addressBar?.addressField?.becomeFirstResponder() 114 | } 115 | 116 | scroll(toTab: newTab, addingTab: true) 117 | 118 | tabCountButton.updateCount(tabList.count) 119 | 120 | return newTab 121 | } 122 | 123 | @objc func addNewTab(withRequest request: URLRequest) { 124 | guard let container = self.containerView else { return } 125 | 126 | let newTab = addNewTab(container: container, focusAddressBar: false) 127 | let _ = newTab.webContainer?.webView?.load(request) 128 | } 129 | 130 | @objc func setUpTabConstraints() { 131 | let tabWidth = TabContainerView.defaultTabWidth 132 | 133 | for (i, tab) in tabList.enumerated() { 134 | tab.snp.remakeConstraints { (make) in 135 | make.top.bottom.equalTo(self.tabScrollView) 136 | make.height.equalTo(TabContainerView.defaultTabHeight) 137 | if i > 0 { 138 | let lastTab = self.tabList[i - 1] 139 | make.left.equalTo(lastTab.snp.right).offset(-6) 140 | } else { 141 | make.left.equalTo(self.tabScrollView) 142 | } 143 | make.width.equalTo(tabWidth) 144 | } 145 | } 146 | 147 | UIView.animate(withDuration: 0.25, animations: { 148 | self.layoutIfNeeded() 149 | }) { _ in 150 | self.tabScrollView.contentSize = CGSize(width: TabContainerView.defaultTabWidth * CGFloat(self.tabList.count) - 151 | CGFloat((self.tabList.count - 1) * 6), 152 | height: TabContainerView.defaultTabHeight) 153 | } 154 | } 155 | 156 | @objc func setTabColors() { 157 | for (i, tab) in tabList.enumerated() { 158 | tab.backgroundColor = (i == selectedTabIndex) ? Colors.radiumGray : Colors.radiumUnselected 159 | } 160 | } 161 | 162 | func scroll(toTab tab: TabView, addingTab: Bool = false) { 163 | guard tabScrollView.contentSize.width > tabScrollView.frame.width else { return } 164 | 165 | let maxOffset = tabScrollView.contentSize.width - tabScrollView.frame.width + ((addingTab) ? TabContainerView.defaultTabWidth : 0) 166 | tabScrollView.setContentOffset(CGPoint(x: min(maxOffset, tab.frame.origin.x), y: 0), animated: true) 167 | } 168 | 169 | // MARK: - Tab Delegate 170 | 171 | @objc func didTap(tab: TabView) { 172 | let prevIndex = selectedTabIndex 173 | if let index = tabList.firstIndex(of: tab) { 174 | selectedTabIndex = index 175 | 176 | var prevTab: TabView? 177 | if tabList.count > 1 { 178 | prevTab = tabList[prevIndex] 179 | } 180 | 181 | switchVisibleWebView(prevTab: prevTab, newTab: tab) 182 | 183 | scroll(toTab: tab) 184 | } 185 | } 186 | 187 | @objc func switchVisibleWebView(prevTab: TabView?, newTab: TabView) { 188 | prevTab?.webContainer?.removeFromView() 189 | newTab.webContainer?.addToView() 190 | 191 | let attrUrl = WebViewManager.shared.getColoredURL(url: newTab.webContainer?.webView?.url) 192 | if attrUrl.string != "" { 193 | addressBar?.setAttributedAddressText(attrUrl) 194 | } else { 195 | addressBar?.setAddressText(newTab.webContainer?.webView?.url?.absoluteString) 196 | } 197 | } 198 | 199 | @objc func close(tab: TabView) -> Bool { 200 | guard tabList.count > 1 else { return false } 201 | guard let indexToRemove = tabList.firstIndex(of: tab) else { return false } 202 | 203 | tabList.remove(at: indexToRemove) 204 | tab.removeFromSuperview() 205 | if selectedTabIndex >= indexToRemove { 206 | selectedTabIndex = max(0, selectedTabIndex - 1) 207 | switchVisibleWebView(prevTab: tab, newTab: tabList[selectedTabIndex]) 208 | } 209 | 210 | setUpTabConstraints() 211 | 212 | tabCountButton.updateCount(tabList.count) 213 | 214 | return true 215 | } 216 | 217 | // MARK: - Web Navigation 218 | 219 | @objc func loadQuery(string: String?) { 220 | guard let string = string else { return } 221 | 222 | if addressBar?.addressField?.text != string { 223 | addressBar?.setAddressText(string) 224 | } 225 | 226 | let tab = tabList[selectedTabIndex] 227 | tab.webContainer?.loadQuery(string: string) 228 | } 229 | 230 | @objc func goBack(sender: UIButton) { 231 | let tab = tabList[selectedTabIndex] 232 | let _ = tab.webContainer?.webView?.goBack() 233 | tab.webContainer?.finishedLoadUpdates() 234 | } 235 | 236 | @objc func goForward(sender: UIButton) { 237 | let tab = tabList[selectedTabIndex] 238 | let _ = tab.webContainer?.webView?.goForward() 239 | tab.webContainer?.finishedLoadUpdates() 240 | } 241 | 242 | @objc func refresh(sender: UIButton) { 243 | let tab = tabList[selectedTabIndex] 244 | let _ = tab.webContainer?.webView?.reload() 245 | } 246 | 247 | @objc func updateNavButtons() { 248 | let tab = tabList[selectedTabIndex] 249 | 250 | addressBar?.backButton?.isEnabled = tab.webContainer?.webView?.canGoBack ?? false 251 | addressBar?.backButton?.tintColor = (tab.webContainer?.webView?.canGoBack ?? false) ? .black : .lightGray 252 | addressBar?.forwardButton?.isEnabled = tab.webContainer?.webView?.canGoForward ?? false 253 | addressBar?.forwardButton?.tintColor = (tab.webContainer?.webView?.canGoForward ?? false) ? .black : .lightGray 254 | } 255 | 256 | // MARK: - Data Managment 257 | 258 | @objc func saveBrowsingSession() { 259 | let session = BrowsingSession() 260 | let models = List() 261 | for tab in tabList { 262 | let model = URLModel() 263 | model.urlString = tab.webContainer?.webView?.url?.absoluteString ?? "" 264 | model.pageTitle = tab.tabTitle ?? "" 265 | models.append(model) 266 | } 267 | session.tabs.append(objectsIn: models) 268 | session.selectedTabIndex = selectedTabIndex 269 | 270 | do { 271 | let realm = try Realm() 272 | try realm.write { 273 | let _ = models.map { 274 | realm.add($0) 275 | } 276 | realm.add(session) 277 | } 278 | } catch let error { 279 | logRealmError(error: error) 280 | } 281 | } 282 | 283 | @objc func loadBrowsingSession() { 284 | var tabModels: List? 285 | var session: BrowsingSession? 286 | var realm: Realm? 287 | do { 288 | realm = try Realm() 289 | session = realm?.objects(BrowsingSession.self).first 290 | tabModels = session?.tabs 291 | } catch let error { 292 | logRealmError(error: error) 293 | } 294 | 295 | guard tabModels != nil else { 296 | let _ = addNewTab(container: containerView!) 297 | return 298 | } 299 | 300 | for model in tabModels! { 301 | let request = URLRequest(url: URL(string: model.urlString)!) 302 | addNewTab(withRequest: request) 303 | } 304 | didTap(tab: tabList[session!.selectedTabIndex]) 305 | 306 | // Remove data from database 307 | do { 308 | try realm?.write { 309 | realm?.delete(tabModels!) 310 | realm?.delete(session!) 311 | } 312 | } catch let error { 313 | logRealmError(error: error) 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /RadiumBrowser/TabCountButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabCountButton.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 11/7/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TabCountButton: UIButton { 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | setTitleColor(.black, for: .normal) 17 | 18 | layer.borderColor = UIColor.black.cgColor 19 | layer.borderWidth = 1.5 20 | layer.cornerRadius = 4 21 | 22 | setTitle("0", for: .normal) 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | func updateCount(_ newCount: Int) { 30 | setTitle("\(newCount)", for: .normal) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /RadiumBrowser/TabTrayViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabTrayViewController.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 11/7/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | class TabTrayViewController: UIViewController { 13 | 14 | static let identifier = "TabTrayIdentifier" 15 | 16 | var collectionView: UICollectionView! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | view.backgroundColor = Colors.radiumUnselected 22 | 23 | let flowLayout = UICollectionViewFlowLayout() 24 | flowLayout.itemSize = CGSize(width: UIScreen.main.bounds.width * 0.4, height: UIScreen.main.bounds.height * 0.4) 25 | flowLayout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) 26 | 27 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout).then { 28 | $0.dataSource = self 29 | $0.delegate = self 30 | $0.backgroundColor = Colors.radiumUnselected 31 | 32 | self.view.addSubview($0) 33 | $0.snp.makeConstraints { make in 34 | if #available(iOS 11.0, *) { 35 | make.edges.equalTo(self.view.safeAreaLayoutGuide) 36 | } else { 37 | make.edges.equalTo(self.view) 38 | } 39 | } 40 | } 41 | 42 | collectionView.register(TabCollectionViewCell.self, forCellWithReuseIdentifier: TabTrayViewController.identifier) 43 | collectionView.reloadData() 44 | } 45 | 46 | override func didReceiveMemoryWarning() { 47 | super.didReceiveMemoryWarning() 48 | // Dispose of any resources that can be recreated. 49 | } 50 | 51 | } 52 | 53 | extension TabTrayViewController: UICollectionViewDataSource, UICollectionViewDelegate { 54 | func numberOfSections(in collectionView: UICollectionView) -> Int { 55 | return 1 56 | } 57 | 58 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 59 | return TabContainerView.currentInstance?.tabList.count ?? 0 60 | } 61 | 62 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 63 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TabTrayViewController.identifier, for: indexPath) as? TabCollectionViewCell 64 | 65 | let tab = TabContainerView.currentInstance?.tabList[indexPath.item] 66 | 67 | cell?.screenshotView.image = tab?.webContainer?.currentScreenshot ?? #imageLiteral(resourceName: "globe") 68 | if let faviconURLString = tab?.webContainer?.favicon?.getPreferredURL(), let faviconURL = URL(string: faviconURLString) { 69 | cell?.faviconView.sd_setImage(with: faviconURL, placeholderImage: #imageLiteral(resourceName: "globe")) 70 | } else { 71 | cell?.faviconView.image = #imageLiteral(resourceName: "globe") 72 | } 73 | cell?.pageTitle.text = tab?.webContainer?.webView?.title 74 | 75 | cell?.closeTabButton.tag = indexPath.item 76 | cell?.delegate = self 77 | 78 | if tab == TabContainerView.currentInstance?.currentTab { 79 | cell?.layer.borderWidth = 2 80 | cell?.layer.borderColor = UIColor.black.cgColor 81 | } else { 82 | cell?.layer.borderColor = UIColor.clear.cgColor 83 | } 84 | 85 | return cell! 86 | } 87 | 88 | func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 89 | return true 90 | } 91 | 92 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 93 | guard let tab = TabContainerView.currentInstance?.tabList[indexPath.item] else { return } 94 | 95 | TabContainerView.currentInstance?.didTap(tab: tab) 96 | self.dismiss(animated: true, completion: nil) 97 | } 98 | } 99 | 100 | extension TabTrayViewController: TabTrayCellDelegate { 101 | func didTapCloseBtn(tabCell: TabCollectionViewCell, tag: Int) { 102 | guard let tab = TabContainerView.currentInstance?.tabList[tag] else { return } 103 | 104 | if TabContainerView.currentInstance?.close(tab: tab) ?? false { 105 | collectionView.performBatchUpdates({ 106 | self.collectionView.deleteItems(at: [IndexPath(item: tag, section: 0)]) 107 | }, completion: { _ in 108 | self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems) 109 | }) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /RadiumBrowser/TabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabView.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol TabViewDelegate: class { 12 | func didTap(tab: TabView) 13 | func close(tab: TabView) -> Bool 14 | } 15 | 16 | class TabView: UIView, UIGestureRecognizerDelegate { 17 | 18 | @objc var shapeLayer: CAShapeLayer? 19 | 20 | @objc var tabTitle: String? { 21 | didSet { 22 | tabTitleLabel?.text = tabTitle 23 | } 24 | } 25 | @objc var tabTitleLabel: UILabel? 26 | @objc var tabImageView: UIImageView? 27 | 28 | weak var delegate: TabViewDelegate? 29 | 30 | @objc var webContainer: WebContainer? 31 | 32 | @objc init(parentView: UIView) { 33 | super.init(frame: .zero) 34 | 35 | self.backgroundColor = Colors.radiumGray 36 | 37 | let closeBtn = UIButton().then { [unowned self] in 38 | $0.setImage(UIImage.imageFrom(systemItem: .stop)?.makeThumbnailOfSize(size: CGSize(width: 15, height: 15)), for: .normal) 39 | $0.addTarget(self, action: #selector(self.close(sender:)), for: .touchUpInside) 40 | 41 | self.addSubview($0) 42 | $0.snp.makeConstraints { (make) in 43 | make.height.equalTo(self).offset(-25) 44 | make.width.equalTo(self.snp.height).offset(-25) 45 | make.right.equalTo(self).offset(-14) 46 | make.centerY.equalTo(self) 47 | } 48 | } 49 | 50 | tabImageView = UIImageView().then { [unowned self] in 51 | $0.image = UIImage(named: "globe") 52 | $0.contentMode = .scaleAspectFit 53 | 54 | self.addSubview($0) 55 | $0.snp.makeConstraints { (make) in 56 | make.width.equalTo(15) 57 | make.height.equalTo(15) 58 | make.left.equalTo(self).offset(13) 59 | make.centerY.equalTo(self) 60 | } 61 | } 62 | 63 | tabTitleLabel = UILabel().then { [unowned self] in 64 | $0.text = "New Tab" 65 | $0.font = UIFont.systemFont(ofSize: UIFont.systemFontSize - 0.5) 66 | 67 | self.addSubview($0) 68 | $0.snp.makeConstraints { (make) in 69 | make.left.equalTo(self.tabImageView!.snp.right).offset(4) 70 | make.top.equalTo(self).offset(6) 71 | make.bottom.equalTo(self).offset(-6) 72 | make.right.equalTo(closeBtn.snp.left).offset(-4) 73 | } 74 | } 75 | 76 | let gesture = UITapGestureRecognizer() 77 | gesture.delegate = self 78 | gesture.addTarget(self, action: #selector(tappedTab)) 79 | gesture.cancelsTouchesInView = false 80 | self.addGestureRecognizer(gesture) 81 | self.isUserInteractionEnabled = true 82 | 83 | webContainer = WebContainer(parent: parentView) 84 | webContainer?.tabView = self 85 | } 86 | 87 | required init?(coder aDecoder: NSCoder) { 88 | fatalError("init(coder:) has not been implemented") 89 | } 90 | 91 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 92 | if touch.view is UIControl { 93 | return false 94 | } 95 | 96 | return true 97 | } 98 | 99 | override func layoutSubviews() { 100 | super.layoutSubviews() 101 | 102 | self.blendCorner(corner: .All, shapeLayer: &shapeLayer, length: 10) 103 | } 104 | 105 | @objc func tappedTab(sender: UITapGestureRecognizer) { 106 | delegate?.didTap(tab: self) 107 | } 108 | 109 | @objc func close(sender: UIButton) { 110 | _ = delegate?.close(tab: self) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /RadiumBrowser/UIImageExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageExtensions.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | @objc class func imageFrom(systemItem: UIBarButtonItem.SystemItem) -> UIImage? { 13 | let tempItem = UIBarButtonItem(barButtonSystemItem: systemItem, target: nil, action: nil) 14 | 15 | // add to toolbar and render it 16 | let bar = UIToolbar() 17 | bar.setItems([tempItem], animated: false) 18 | bar.snapshotView(afterScreenUpdates: true) 19 | 20 | // got image from real uibutton 21 | if let itemView = tempItem.value(forKey: "view") as? UIView { 22 | for view in itemView.subviews { 23 | if let button = view as? UIButton, let imageView = button.imageView { 24 | return imageView.image 25 | } 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | 32 | @objc func makeThumbnailOfSize(size: CGSize) -> UIImage { 33 | UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale) 34 | 35 | self.draw(in: CGRect(origin: .zero, size: size)) 36 | let newThumbnail = UIGraphicsGetImageFromCurrentImageContext() 37 | 38 | UIGraphicsEndImageContext() 39 | 40 | return newThumbnail! 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /RadiumBrowser/UIViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewExtensions.swift 3 | // RadiumBrowser 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | public enum Corner: Int { 14 | case TopRight 15 | case TopLeft 16 | case BottomRight 17 | case BottomLeft 18 | case All 19 | } 20 | 21 | public func blendCorner(corner: Corner, shapeLayer: inout CAShapeLayer?, length: CGFloat = 8.0) { 22 | let maskLayer = CAShapeLayer() 23 | let path: CGPath 24 | let outlinePath: UIBezierPath 25 | switch corner { 26 | case .All: 27 | (path, outlinePath) = self.makeAnglePathWithRect(self.bounds, topLeftSize: length, topRightSize: length, 28 | bottomLeftSize: 0.0, bottomRightSize: 0.0) 29 | case .TopRight: 30 | (path, outlinePath) = self.makeAnglePathWithRect(self.bounds, topLeftSize: 0.0, topRightSize: length, 31 | bottomLeftSize: 0.0, bottomRightSize: 0.0) 32 | case .TopLeft: 33 | (path, outlinePath) = self.makeAnglePathWithRect(self.bounds, topLeftSize: length, topRightSize: 0.0, 34 | bottomLeftSize: 0.0, bottomRightSize: 0.0) 35 | case .BottomRight: 36 | (path, outlinePath) = self.makeAnglePathWithRect(self.bounds, topLeftSize: 0.0, topRightSize: 0.0, 37 | bottomLeftSize: 0.0, bottomRightSize: length) 38 | case .BottomLeft: 39 | (path, outlinePath) = self.makeAnglePathWithRect(self.bounds, topLeftSize: 0.0, topRightSize: 0.0, 40 | bottomLeftSize: length, bottomRightSize: 0.0) 41 | } 42 | maskLayer.path = path 43 | self.layer.mask = maskLayer 44 | 45 | if shapeLayer == nil { 46 | shapeLayer = CAShapeLayer() 47 | } 48 | shapeLayer?.frame = self.bounds 49 | shapeLayer?.path = outlinePath.cgPath 50 | shapeLayer?.lineWidth = 1.0 51 | shapeLayer?.strokeColor = UIColor.gray.cgColor 52 | shapeLayer?.fillColor = UIColor.clear.cgColor 53 | if shapeLayer?.superlayer == nil { 54 | self.layer.addSublayer(shapeLayer!) 55 | } 56 | } 57 | 58 | private func makeAnglePathWithRect(_ rect: CGRect, topLeftSize tl: CGFloat, topRightSize tr: CGFloat, 59 | bottomLeftSize bl: CGFloat, bottomRightSize br: CGFloat) -> (CGPath, UIBezierPath) { 60 | var points = [CGPoint]() 61 | 62 | points.append(CGPoint(x: rect.origin.x + tl, y: rect.origin.y)) 63 | points.append(CGPoint(x: rect.origin.x + rect.size.width - tr, y: rect.origin.y)) 64 | points.append(CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height)) 65 | points.append(CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height - br)) 66 | points.append(CGPoint(x: rect.origin.x + rect.size.width - br, y: rect.origin.y + rect.size.height)) 67 | points.append(CGPoint(x: rect.origin.x + bl, y: rect.origin.y + rect.size.height)) 68 | points.append(CGPoint(x: rect.origin.x, y: rect.origin.y + rect.size.height - bl)) 69 | points.append(CGPoint(x: rect.origin.x, y: rect.origin.y + rect.size.height)) 70 | 71 | let outlinePoints = points[0.. UIImage? { 14 | UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0) 15 | self.drawHierarchy(in: self.bounds, afterScreenUpdates: true) 16 | let img = UIGraphicsGetImageFromCurrentImageContext() 17 | UIGraphicsEndImageContext() 18 | return img 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /RadiumBrowser/WebContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebContainer.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebKit 11 | import RealmSwift 12 | 13 | class WebContainer: UIView, WKNavigationDelegate, WKUIDelegate { 14 | 15 | @objc weak var parentView: UIView? 16 | @objc var webView: WKWebView? 17 | @objc var isObserving = false 18 | 19 | @objc weak var tabView: TabView? 20 | var favicon: Favicon? 21 | var currentScreenshot: UIImage? 22 | @objc var builtinExtensions: [BuiltinExtension]? 23 | 24 | @objc var progressView: UIProgressView? 25 | 26 | @objc var notificationToken: NotificationToken! 27 | 28 | deinit { 29 | if isObserving { 30 | webView?.removeObserver(self, forKeyPath: "estimatedProgress") 31 | webView?.removeObserver(self, forKeyPath: "title") 32 | } 33 | notificationToken.invalidate() 34 | NotificationCenter.default.removeObserver(self) 35 | } 36 | 37 | @objc init(parent: UIView) { 38 | super.init(frame: .zero) 39 | 40 | NotificationCenter.default.addObserver(self, selector: #selector(adBlockChanged), name: NSNotification.Name.adBlockSettingsChanged, object: nil) 41 | 42 | self.parentView = parent 43 | 44 | backgroundColor = .white 45 | 46 | webView = WKWebView(frame: .zero, configuration: loadConfiguration()).then { [unowned self] in 47 | $0.allowsLinkPreview = true 48 | $0.allowsBackForwardNavigationGestures = true 49 | $0.navigationDelegate = self 50 | $0.uiDelegate = self 51 | 52 | self.addSubview($0) 53 | $0.snp.makeConstraints { (make) in 54 | make.edges.equalTo(self) 55 | } 56 | } 57 | 58 | progressView = UIProgressView().then { [unowned self] in 59 | $0.isHidden = true 60 | 61 | self.addSubview($0) 62 | $0.snp.makeConstraints { (make) in 63 | make.width.equalTo(self) 64 | make.top.equalTo(self) 65 | make.left.equalTo(self) 66 | } 67 | } 68 | 69 | do { 70 | let realm = try Realm() 71 | self.notificationToken = realm.observe { _, _ in 72 | self.reloadExtensions() 73 | } 74 | } catch let error as NSError { 75 | print("Error occured opening realm: \(error.localizedDescription)") 76 | } 77 | 78 | loadBuiltins() 79 | 80 | loadAdBlocking { [weak self] in 81 | if self?.webView?.url == nil { 82 | let _ = self?.webView?.load(URLRequest(url: URL(string: "http://localhost:8080")!)) 83 | } 84 | } 85 | } 86 | 87 | required init?(coder aDecoder: NSCoder) { 88 | fatalError("init(coder:) has not been implemented") 89 | } 90 | 91 | // MARK: - Configuration Setup 92 | 93 | @objc func loadBuiltins() { 94 | builtinExtensions = WebViewManager.shared.loadBuiltinExtensions(webContainer: self) 95 | builtinExtensions?.forEach { 96 | if let handler = $0 as? WKScriptMessageHandler, let handlerName = $0.scriptHandlerName { 97 | webView?.configuration.userContentController.add(handler, name: handlerName) 98 | } 99 | } 100 | } 101 | 102 | @objc func loadConfiguration() -> WKWebViewConfiguration { 103 | let config = WKWebViewConfiguration() 104 | 105 | let contentController = WKUserContentController() 106 | loadExtensions().forEach { 107 | contentController.addUserScript($0) 108 | } 109 | 110 | config.userContentController = contentController 111 | config.processPool = WebViewManager.sharedProcessPool 112 | 113 | return config 114 | } 115 | 116 | @objc func loadExtensions() -> [WKUserScript] { 117 | var extensions = [WKUserScript]() 118 | 119 | var models: Results? 120 | do { 121 | let realm = try Realm() 122 | models = realm.objects(ExtensionModel.self).filter("active = true") 123 | } catch let error { 124 | print("Could not load extensions: \(error.localizedDescription)") 125 | return [] 126 | } 127 | 128 | for model in models! { 129 | let injectionTime: WKUserScriptInjectionTime = (model.injectionTime == 0) ? .atDocumentStart : .atDocumentEnd 130 | let script = WKUserScript(source: model.source, injectionTime: injectionTime, forMainFrameOnly: false) 131 | extensions.append(script) 132 | } 133 | 134 | return extensions 135 | } 136 | 137 | @objc func reloadExtensions() { 138 | // Called when a new extension is added to Realm 139 | webView?.configuration.userContentController.removeAllUserScripts() 140 | loadExtensions().forEach { 141 | webView?.configuration.userContentController.addUserScript($0) 142 | } 143 | builtinExtensions?.forEach { 144 | if let userScript = $0.webScript { 145 | webView?.configuration.userContentController.addUserScript(userScript) 146 | } 147 | } 148 | } 149 | 150 | func loadAdBlocking(completion: @escaping (() -> ())) { 151 | if #available(iOS 11.0, *), AdBlockManager.shared.shouldBlockAds() { 152 | let group = DispatchGroup() 153 | 154 | for hostFile in HostFileNames.allValues { 155 | group.enter() 156 | AdBlockManager.shared.setupAdBlock(forKey: hostFile.rawValue, filename: hostFile.rawValue, webView: webView) { 157 | group.leave() 158 | } 159 | } 160 | 161 | group.enter() 162 | AdBlockManager.shared.setupAdBlockFromStringLiteral(forWebView: self.webView) { 163 | group.leave() 164 | } 165 | 166 | group.notify(queue: .main, execute: { 167 | completion() 168 | }) 169 | } else { 170 | completion() 171 | } 172 | } 173 | 174 | @objc func adBlockChanged() { 175 | guard #available(iOS 11.0, *) else { return } 176 | 177 | let currentRequest = URLRequest(url: webView!.url!) 178 | if AdBlockManager.shared.shouldBlockAds() { 179 | loadAdBlocking { 180 | self.webView?.load(currentRequest) 181 | } 182 | } else { 183 | AdBlockManager.shared.disableAdBlock(forWebView: webView) 184 | webView?.load(currentRequest) 185 | } 186 | } 187 | 188 | // MARK: - View Managment 189 | 190 | @objc func addToView() { 191 | guard let _ = parentView else { return } 192 | 193 | parentView?.addSubview(self) 194 | self.snp.makeConstraints { (make) in 195 | make.edges.equalTo(parentView!) 196 | } 197 | 198 | if !isObserving { 199 | webView?.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil) 200 | webView?.addObserver(self, forKeyPath: "title", options: .new, context: nil) 201 | isObserving = true 202 | } 203 | } 204 | 205 | @objc func removeFromView() { 206 | guard let _ = parentView else { return } 207 | 208 | takeScreenshot() 209 | 210 | // Remove ourself as the observer 211 | if isObserving { 212 | webView?.removeObserver(self, forKeyPath: "estimatedProgress") 213 | webView?.removeObserver(self, forKeyPath: "title") 214 | isObserving = false 215 | progressView?.setProgress(0, animated: false) 216 | progressView?.isHidden = true 217 | } 218 | 219 | self.removeFromSuperview() 220 | } 221 | 222 | @objc func loadQuery(string: String) { 223 | var urlString = string 224 | if !urlString.isURL() { 225 | let searchTerms = urlString.replacingOccurrences(of: " ", with: "+") 226 | let searchUrl = UserDefaults.standard.string(forKey: SettingsKeys.searchEngineUrl)! 227 | urlString = searchUrl + searchTerms 228 | } else if !urlString.hasPrefix("http://") && !urlString.hasPrefix("https://") { 229 | urlString = "http://" + urlString 230 | } 231 | 232 | if let url = URL(string: urlString) { 233 | let _ = webView?.load(URLRequest(url: url)) 234 | } 235 | } 236 | 237 | func takeScreenshot() { 238 | currentScreenshot = screenshot() 239 | } 240 | 241 | // MARK: - Webview Delegate 242 | 243 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 244 | if keyPath == "estimatedProgress" { 245 | progressView?.isHidden = webView?.estimatedProgress == 1 246 | progressView?.setProgress(Float(webView!.estimatedProgress), animated: true) 247 | 248 | if webView?.estimatedProgress == 1 { 249 | progressView?.setProgress(0, animated: false) 250 | } 251 | } else if keyPath == "title" { 252 | tabView?.tabTitle = webView?.title 253 | } 254 | } 255 | 256 | func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { 257 | favicon = nil 258 | } 259 | 260 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 261 | finishedLoadUpdates() 262 | } 263 | 264 | @objc func finishedLoadUpdates() { 265 | guard let webView = webView else { return } 266 | 267 | WebViewManager.shared.logPageVisit(url: webView.url?.absoluteString, pageTitle: webView.title) 268 | 269 | tabView?.tabTitle = webView.title 270 | tryToGetFavicon(for: webView.url) 271 | 272 | if let tabContainer = TabContainerView.currentInstance, isObserving { 273 | let attrUrl = WebViewManager.shared.getColoredURL(url: webView.url) 274 | if attrUrl.string == "" { 275 | tabContainer.addressBar?.setAddressText(webView.url?.absoluteString) 276 | } else { 277 | tabContainer.addressBar?.setAttributedAddressText(attrUrl) 278 | } 279 | tabContainer.updateNavButtons() 280 | } 281 | } 282 | 283 | func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, 284 | for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { 285 | if let tabContainer = TabContainerView.currentInstance, navigationAction.targetFrame == nil { 286 | tabContainer.addNewTab(withRequest: navigationAction.request) 287 | } 288 | return nil 289 | } 290 | 291 | func webViewDidClose(_ webView: WKWebView) { 292 | if let tabContainer = TabContainerView.currentInstance { 293 | _ = tabContainer.close(tab: tabView!) 294 | } 295 | } 296 | 297 | func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { 298 | handleError(error as NSError) 299 | } 300 | 301 | func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { 302 | handleError(error as NSError) 303 | } 304 | 305 | func handleError(_ error: NSError) { 306 | print(error.localizedDescription) 307 | // if let failUrl = error.userInfo["NSErrorFailingURLStringKey"] as? String, let url = URL(string: failUrl), !failUrl.contains("localhost") { 308 | // UIApplication.shared.open(url, options: convertToUIApplicationOpenExternalURLOptionsKeyDictionary([:]), completionHandler: { success in 309 | // if success { 310 | // print("openURL succeeded") 311 | // } 312 | // }) 313 | // } 314 | } 315 | 316 | // MARK: - Alert Methods 317 | 318 | func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, 319 | initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { 320 | let av = UIAlertController(title: webView.title, message: message, preferredStyle: .alert) 321 | av.addAction(UIAlertAction(title: "Ok", style: .default, handler: { _ in 322 | completionHandler() 323 | })) 324 | self.parentViewController?.present(av, animated: true, completion: nil) 325 | } 326 | 327 | func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, 328 | initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { 329 | let av = UIAlertController(title: webView.title, message: message, preferredStyle: .alert) 330 | av.addAction(UIAlertAction(title: "Ok", style: .default, handler: { _ in 331 | completionHandler(true) 332 | })) 333 | av.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in 334 | completionHandler(false) 335 | })) 336 | self.parentViewController?.present(av, animated: true, completion: nil) 337 | } 338 | 339 | func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, 340 | defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { 341 | let av = UIAlertController(title: webView.title, message: prompt, preferredStyle: .alert) 342 | av.addTextField(configurationHandler: { (textField) in 343 | textField.placeholder = prompt 344 | textField.text = defaultText 345 | }) 346 | av.addAction(UIAlertAction(title: "Ok", style: .default, handler: { _ in 347 | completionHandler(av.textFields?.first?.text) 348 | })) 349 | av.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in 350 | completionHandler(nil) 351 | })) 352 | self.parentViewController?.present(av, animated: true, completion: nil) 353 | } 354 | 355 | // MARK: - Helper methods 356 | 357 | @objc func tryToGetFavicon(for url: URL?) { 358 | if let faviconURL = favicon?.iconURL { 359 | tabView?.tabImageView?.sd_setImage(with: URL(string: faviconURL), placeholderImage: UIImage(named: "globe")) 360 | return 361 | } 362 | 363 | guard let url = url else { return } 364 | guard let scheme = url.scheme else { return } 365 | guard let host = url.host else { return } 366 | 367 | let faviconURL = scheme + "://" + host + "/favicon.ico" 368 | 369 | tabView?.tabImageView?.sd_setImage(with: URL(string: faviconURL), placeholderImage: UIImage(named: "globe")) 370 | } 371 | } 372 | 373 | // Helper function inserted by Swift 4.2 migrator. 374 | fileprivate func convertToUIApplicationOpenExternalURLOptionsKeyDictionary(_ input: [String: Any]) -> [UIApplication.OpenExternalURLOptionsKey: Any] { 375 | return Dictionary(uniqueKeysWithValues: input.map { key, value in (UIApplication.OpenExternalURLOptionsKey(rawValue: key), value)}) 376 | } 377 | -------------------------------------------------------------------------------- /RadiumBrowser/WebServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebServer.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 11/1/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import GCDWebServer 11 | import RealmSwift 12 | 13 | class WebServer { 14 | static let shared = WebServer() 15 | 16 | let webServer = GCDWebServer() 17 | 18 | let newTabHTMLStart = """ 19 | 20 | 21 | 22 | New Tab 23 | 76 | 77 | 78 |

New Tab

79 | """ 80 | let newTabEnd = """ 81 | 84 | 85 | """ 86 | 87 | init() { 88 | webServer.addDefaultHandler(forMethod: "GET", request: GCDWebServerRequest.self, processBlock: { _ in 89 | return GCDWebServerDataResponse(html: self.getNewTabHTMLString()) 90 | }) 91 | webServer.addHandler(forMethod: "GET", path: "/noimage", request: GCDWebServerRequest.self, processBlock: { _ in 92 | let img = #imageLiteral(resourceName: "globe") 93 | return GCDWebServerDataResponse(data: img.pngData()!, contentType: "image/png") 94 | }) 95 | webServer.addHandler(forMethod: "GET", path: "/favicon.ico", request: GCDWebServerRequest.self, processBlock: { _ in 96 | let iconsDictionary = Bundle.main.infoDictionary?["CFBundleIcons"] as? NSDictionary 97 | let primaryIconsDictionary = iconsDictionary?["CFBundlePrimaryIcon"] as? NSDictionary 98 | let iconFiles = primaryIconsDictionary!["CFBundleIconFiles"] as! NSArray 99 | // First will be smallest for the device class, last will be the largest for device class 100 | let lastIcon = iconFiles.lastObject as! NSString 101 | let icon = UIImage(named: lastIcon as String)! 102 | return GCDWebServerDataResponse(data: icon.pngData()!, contentType: "image/png") 103 | }) 104 | } 105 | 106 | func startServer() { 107 | webServer.start(withPort: 8080, bonjourName: nil) 108 | } 109 | 110 | func getNewTabHTMLString() -> String { 111 | var result = newTabHTMLStart 112 | 113 | let bookmarks: Results? 114 | do { 115 | let realm = try Realm() 116 | bookmarks = realm.objects(Bookmark.self) 117 | } catch { 118 | bookmarks = nil 119 | print("Error: \(error.localizedDescription)") 120 | } 121 | 122 | if let bookmarks = bookmarks, bookmarks.count > 0 { 123 | result += "
" 124 | for i in 0.. 128 | 129 |

\(bookmarks[i].name)

130 |
131 | """ 132 | } 133 | result += "" 134 | } else { 135 | result += "

Go add some bookmarks to see them here!

" 136 | } 137 | 138 | return result + newTabEnd 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /RadiumBrowser/WebViewManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewManager.swift 3 | // RadiumBrowser 4 | // 5 | // Created by Bradley Slayter on 2/5/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | import WebKit 12 | 13 | typealias ScriptHandler = (WKUserContentController, WKScriptMessage) -> () 14 | 15 | class WebViewManager: NSObject { 16 | @objc static let shared = WebViewManager() 17 | @objc static let sharedProcessPool = WKProcessPool() 18 | 19 | @objc func logPageVisit(url: String?, pageTitle: String?) { 20 | if let urlString = url, let urlObj = URL(string: urlString), urlObj.host == "localhost" { 21 | // We don't want to log any pages we serve ourselves 22 | return 23 | } 24 | 25 | // Check if we should be logging page visits 26 | if !UserDefaults.standard.bool(forKey: SettingsKeys.trackHistory) { 27 | return 28 | } 29 | 30 | let entry = HistoryEntry() 31 | entry.id = UUID().uuidString 32 | entry.pageURL = url ?? "" 33 | entry.pageTitle = pageTitle ?? "" 34 | entry.visitDate = Date() 35 | 36 | do { 37 | let realm = try Realm() 38 | try realm.write { 39 | realm.add(entry) 40 | } 41 | } catch let error { 42 | logRealmError(error: error) 43 | } 44 | } 45 | 46 | @objc func getColoredURL(url: URL?) -> NSAttributedString { 47 | guard let url = url else { return NSAttributedString(string: "") } 48 | guard let _ = url.host else { return NSAttributedString(string: "") } 49 | let urlString = url.absoluteString as NSString 50 | 51 | let mutableAttributedString = NSMutableAttributedString(string: urlString as String, 52 | attributes: [.foregroundColor: UIColor.gray]) 53 | if url.scheme == "https" { 54 | let range = urlString.range(of: url.scheme!) 55 | mutableAttributedString.addAttribute(.foregroundColor, value: Colors.urlGreen, range: range) 56 | } 57 | 58 | let domainRange = urlString.range(of: url.host!) 59 | mutableAttributedString.addAttribute(.foregroundColor, value: UIColor.black, range: domainRange) 60 | 61 | return mutableAttributedString 62 | } 63 | 64 | @objc func loadBuiltinExtensions(webContainer: WebContainer) -> [BuiltinExtension] { 65 | let faviconGetter = FaviconGetter(container: webContainer) 66 | 67 | return [faviconGetter] 68 | } 69 | } 70 | 71 | // Helper function inserted by Swift 4.2 migrator. 72 | fileprivate func convertToOptionalNSAttributedStringKeyDictionary(_ input: [String: Any]?) -> [NSAttributedString.Key: Any]? { 73 | guard let input = input else { return nil } 74 | return Dictionary(uniqueKeysWithValues: input.map { key, value in (NSAttributedString.Key(rawValue: key), value)}) 75 | } 76 | 77 | // Helper function inserted by Swift 4.2 migrator. 78 | fileprivate func convertToNSAttributedStringKey(_ input: String) -> NSAttributedString.Key { 79 | return NSAttributedString.Key(rawValue: input) 80 | } 81 | -------------------------------------------------------------------------------- /RadiumBrowser/adaway.json: -------------------------------------------------------------------------------- 1 | [{"trigger":{"url-filter":".*","if-domain":["lb.usemaxserver.de","tracking.klickthru.com","gsmtop.net","click.buzzcity.net","ads.admoda.com","stats.pflexads.com","a.glcdn.co","wwww.adleads.com","ad.madvertise.de","apps.buzzcity.net","ads.mobgold.com","android.bcfads.com","show.buzzcity.net","api.analytics.omgpop.com","r.edge.inmobicdn.net","www.mmnetwork.mobi","img.ads.huntmad.com","creative1cdn.mobfox.com","admicro2.vcmedia.vn","admicro1.vcmedia.vn","s3.phluant.com","c.vrvm.com","go.vrvm.com","static.estebull.com","mobile.banzai.it","ads.xxxad.net","img.ads.mojiva.com","adcontent.saymedia.com","ads.saymedia.com","ftpcontent.worldnow.com","s0.2mdn.net","img.ads.mocean.mobi","bigmobileads.com","banners.bigmobileads.com","ads.mopub.com","images.mpression.net","images.millennialmedia.com","oasc04012.247realmedia.com","assets.cntdy.mobi","ad.leadboltapps.net","api.airpush.com","ad.where.com","i.tapit.com","cdn1.crispadvertising.com","cdn2.crispadvertising.com","medrx.sensis.com.au","rs-staticart.ybcdn.net","img.ads.taptapnetworks.com","adserver.ubiyoo.com","c753738.r38.cf2.rackcdn.com","edge.reporo.net","ads.n-ws.org","adultmoda.com","ads.smartdevicemedia.com","b.scorecardresearch.com","m.adsymptotic.com","cdn.vdopia.com","api.yp.com","asotrack1.fluentmobile.com","android-sdk31.transpera.com","apps.mobilityware.com","ads.mobilityware.com","ads.admarvel.com","netdna.reporo.net","www.eltrafiko.com","cdn.trafficforce.com","gts-ads.twistbox.com","static.cdn.gtsmobi.com","ads.matomymobile.com","ads.adiquity.com","img.ads.mobilefuse.net","as.adfonic.net","media.mobpartner.mobi","cdn.us.goldspotmedia.com","ads2.mediaarmor.com","cdn.nearbyad.com","ads.ookla.com","mobiledl.adobe.com","ads.flurry.com","gemini.yahoo.com","d3anogn3pbtk4v.cloudfront.net","d3oltyb66oj2v8.cloudfront.net","d2bgg7rjywcwsy.cloudfront.net","a.vserv.mobi","admin.vserv.mobi","c.vserv.mobi","ads.vserv.mobi","sf.vserv.mobi","hybl9bazbc35.pflexads.com","hhbekxxw5d9e.pflexads.com","www.pflexads.com","orencia.pflexads.com","atti.velti.com","ru.velti.com","mwc.velti.com","cdn.celtra.com","ads.celtra.com","cache-ssl.celtra.com","cache.celtra.com","track.celtra.com","wv.inner-active.mobi","cdn1.inner-active.mobi","m2m1.inner-active.mobi","bos-tapreq01.jumptap.com","bos-tapreq02.jumptap.com","bos-tapreq03.jumptap.com","bos-tapreq04.jumptap.com","bos-tapreq05.jumptap.com","bos-tapreq06.jumptap.com","bos-tapreq07.jumptap.com","bos-tapreq08.jumptap.com","bos-tapreq09.jumptap.com","bos-tapreq10.jumptap.com","bos-tapreq11.jumptap.com","bos-tapreq12.jumptap.com","bos-tapreq13.jumptap.com","bos-tapreq14.jumptap.com","bos-tapreq15.jumptap.com","bos-tapreq16.jumptap.com","bos-tapreq17.jumptap.com","bos-tapreq18.jumptap.com","bos-tapreq19.jumptap.com","bos-tapreq20.jumptap.com","web64.jumptap.com","web63.jumptap.com","web65.jumptap.com","bo.jumptap.com","i.jumptap.com","a.applovin.com","d.applovin.com","pdn.applovin.com","mobpartner.mobi","go.mobpartner.mobi","r.mobpartner.mobi","uk-ad2.adinfuse.com","adinfuse.com","go.adinfuse.com","ad1.adinfuse.com","ad2.adinfuse.com","sky.adinfuse.com","orange-fr.adinfuse.com","sky-connect.adinfuse.com","uk-go.adinfuse.com","orangeuk-mc.adinfuse.com","intouch.adinfuse.com","funnel0.adinfuse.com","cvt.mydas.mobi","lp.mydas.mobi","golds.lp.mydas.mobi","suo.lp.mydas.mobi","aio.lp.mydas.mobi","lp.mp.mydas.mobi","media.mydas.mobi","ads.mp.mydas.mobi","neptune.appads.com","neptune1.appads.com","neptune2.appads.com","neptune3.appads.com","saturn.appads.com","saturn1.appads.com","saturn2.appads.com","saturn3.appads.com","jupiter.appads.com","jupiter1.appads.com","jupiter2.appads.com","jupiter3.appads.com","req.appads.com","req1.appads.com","req2.appads.com","req3.appads.com","mc.yandex.ru","an.yandex.ru","swappit.tapad.com","campaign-tapad.s3.amazonaws.com","adsrv1.tapad.com","ads1.mojiva.com","ads2.mojiva.com","ads3.mojiva.com","ads4.mojiva.com","ads5.mojiva.com","i.w.inmobi.com","r.w.inmobi.com","c.w.inmobi.com","adtracker.inmobi.com","china.inmobi.com","japan.inmobi.com","mdn1.phluantmobile.net","mdn2.phluantmobile.net","mdn3.phluantmobile.net","mdn3origin.phluantmobile.net","soma.smaato.net","c29new.smaato.net","c01.smaato.net","c02.smaato.net","c03.smaato.net","c04.smaato.net","c05.smaato.net","c06.smaato.net","c07.smaato.net","c08.smaato.net","c09.smaato.net","c10.smaato.net","c11.smaato.net","c12.smaato.net","c13.smaato.net","c14.smaato.net","c15.smaato.net","c16.smaato.net","c17.smaato.net","c18.smaato.net","c19.smaato.net","c20.smaato.net","c21.smaato.net","c22.smaato.net","c23.smaato.net","c24.smaato.net","c25.smaato.net","c26.smaato.net","c27.smaato.net","c28.smaato.net","c29.smaato.net","c30.smaato.net","c31.smaato.net","c32.smaato.net","c33.smaato.net","c34.smaato.net","c35.smaato.net","c36.smaato.net","c37.smaato.net","c38.smaato.net","c39.smaato.net","c40.smaato.net","c41.smaato.net","c42.smaato.net","c43.smaato.net","c44.smaato.net","c45.smaato.net","c46.smaato.net","c47.smaato.net","c48.smaato.net","c49.smaato.net","c50.smaato.net","c51.smaato.net","c52.smaato.net","c53.smaato.net","c54.smaato.net","c55.smaato.net","c56.smaato.net","c57.smaato.net","c58.smaato.net","c59.smaato.net","c60.smaato.net","f03.smaato.net","f04.smaato.net","f05.smaato.net","f06.smaato.net","f07.smaato.net","f08.smaato.net","f09.smaato.net","f10.smaato.net","f11.smaato.net","f12.smaato.net","f13.smaato.net","f14.smaato.net","f15.smaato.net","f16.smaato.net","f17.smaato.net","f18.smaato.net","f19.smaato.net","f20.smaato.net","f21.smaato.net","f22.smaato.net","f23.smaato.net","f24.smaato.net","f25.smaato.net","f26.smaato.net","f27.smaato.net","f28.smaato.net","f29.smaato.net","f30.smaato.net","f31.smaato.net","f32.smaato.net","f33.smaato.net","f34.smaato.net","f35.smaato.net","f36.smaato.net","f37.smaato.net","f38.smaato.net","f39.smaato.net","f40.smaato.net","f41.smaato.net","f42.smaato.net","f43.smaato.net","f44.smaato.net","f45.smaato.net","f46.smaato.net","f47.smaato.net","f48.smaato.net","f49.smaato.net","f50.smaato.net","f51.smaato.net","f52.smaato.net","f53.smaato.net","f54.smaato.net","f55.smaato.net","f56.smaato.net","f57.smaato.net","f58.smaato.net","f59.smaato.net","f60.smaato.net","img.ads1.mojiva.com","img.ads2.mojiva.com","img.ads3.mojiva.com","img.ads4.mojiva.com","img.ads1.mocean.mobi","img.ads2.mocean.mobi","img.ads3.mocean.mobi","img.ads4.mocean.mobi","akamai.smartadserver.com","cdn1.smartadserver.com","diff.smartadserver.com","diff2.smartadserver.com","diff3.smartadserver.com","eqx.smartadserver.com","im2.smartadserver.com","itx5-publicidad.smartadserver.com","itx5.smartadserver.com","tcy.smartadserver.com","ww129.smartadserver.com","ww13.smartadserver.com","ww14.smartadserver.com","ww234.smartadserver.com","ww251.smartadserver.com","ww264.smartadserver.com","ww302.smartadserver.com","ww362.smartadserver.com","ww370.smartadserver.com","ww381.smartadserver.com","ww392.smartadserver.com","ww55.smartadserver.com","ww57.smartadserver.com","ww84.smartadserver.com","www.smartadserver.com","www2.smartadserver.com","www3.smartadserver.com","www4.smartadserver.com","ads.mobclix.com","data.mobclix.com","s.mobclix.com","ads.mdotm.com","cdn.mdotm.com","ads2.greystripe.com","adsx.greystripe.com","c.greystripe.com","aax-us-east.amazon-adsystem.com","aax-us-west.amazon-adsystem.com","s.amazon-adsystem.com","admarvel.s3.amazonaws.com","html5adkit.plusmo.s3.amazonaws.com","inneractive-assets.s3.amazonaws.com","strikeadcdn.s3.amazonaws.com","a.admob.com","analytics.admob.com","c.admob.com","media.admob.com","p.admob.com","met.adwhirl.com","mob.adwhirl.com","ad-g.doubleclick.net","ad.doubleclick.net","ad.mo.doubleclick.net","doubleclick.net","googleads.g.doubleclick.net","pagead.googlesyndication.com","pagead1.googlesyndication.com","pagead2.googlesyndication.com","events.foreseeresults.com","survey.foreseeresults.com","m.quantserve.com","ad.leadboltmobile.net","mobileads.msn.com","img.adecorp.co.kr","us0.adlibr.com","ad.parrot.mable-inc.com","aos.wall.youmi.net","au.youmi.net","coconuts.boy.jp","iacpromotion.s3.amazonaws.com","plugin.2easydroid.com","adimg3.search.naver.net","st.a-link.co.kr","cdn.ajillionmax.com","dispatch.admixer.co.kr","ifc.inmobi.com","thinknear-hosted.thinknearhub.com","ads.adadapted.com","analytics.localytics.com","a.medialytics.com","c.medialytics.com","cdn.creative.medialytics.com","p.medialytics.com","px.cdn.creative.medialytics.com","t.medialytics.com","google-analytics.com","googlesyndication.com","applift.com","trackersimulator.org","eviltracker.net","do-not-tracker.org"]},"action":{"type":"block"}}] -------------------------------------------------------------------------------- /RadiumBrowser/topdomains.txt: -------------------------------------------------------------------------------- 1 | google.com 2 | facebook.com 3 | amazon.com 4 | youtube.com 5 | yahoo.com 6 | ebay.com 7 | wikipedia.org 8 | twitter.com 9 | reddit.com 10 | go.com 11 | craigslist.org 12 | live.com 13 | netflix.com 14 | pinterest.com 15 | bing.com 16 | linkedin.com 17 | imgur.com 18 | espn.go.com 19 | walmart.com 20 | tumblr.com 21 | target.com 22 | paypal.com 23 | cnn.com 24 | chase.com 25 | instagram.com 26 | bestbuy.com 27 | blogspot.com 28 | nytimes.com 29 | msn.com 30 | imdb.com 31 | apple.com 32 | bankofamerica.com 33 | diply.com 34 | huffingtonpost.com 35 | yelp.com 36 | wellsfargo.com 37 | etsy.com 38 | weather.com 39 | wordpress.com 40 | buzzfeed.com 41 | zillow.com 42 | kohls.com 43 | aol.com 44 | homedepot.com 45 | foxnews.com 46 | microsoft.com 47 | comcast.net 48 | wikia.com 49 | groupon.com 50 | macys.com 51 | washingtonpost.com 52 | outbrain.com 53 | microsoftonline.com 54 | xfinity.com 55 | usps.com 56 | hulu.com 57 | americanexpress.com 58 | slickdeals.net 59 | pandora.com 60 | office.com 61 | cnet.com 62 | indeed.com 63 | capitalone.com 64 | nfl.com 65 | ups.com 66 | ask.com 67 | verizonwireless.com 68 | newegg.com 69 | usatoday.com 70 | forbes.com 71 | dailymail.co.uk 72 | dropbox.com 73 | att.com 74 | costco.com 75 | gfycat.com 76 | lowes.com 77 | gap.com 78 | about.com 79 | tripadvisor.com 80 | fedex.com 81 | baidu.com 82 | vice.com 83 | nordstrom.com 84 | adobe.com 85 | bbc.com 86 | twitch.tv 87 | allrecipes.com 88 | retailmenot.com 89 | stackoverflow.com 90 | citi.com 91 | sears.com 92 | jcpenney.com 93 | webmd.com 94 | ijreview.com 95 | nih.gov 96 | answers.com 97 | foodnetwork.com 98 | discovercard.com 99 | cbssports.com 100 | overstock.com 101 | businessinsider.com 102 | office365.com 103 | theguardian.com 104 | staples.com 105 | bleacherreport.com 106 | toysrus.com 107 | verizon.com 108 | github.com 109 | wayfair.com 110 | salesforce.com 111 | zulily.com 112 | wsj.com 113 | flickr.com 114 | goodreads.com 115 | realtor.com 116 | nbcnews.com 117 | ebates.com 118 | ancestry.com 119 | wunderground.com 120 | instructure.com 121 | people.com 122 | stackexchange.com 123 | drudgereport.com 124 | fidelity.com 125 | southwest.com 126 | deviantart.com 127 | thesaurus.com 128 | intuit.com 129 | woot.com 130 | pch.com 131 | soundcloud.com 132 | force.com 133 | samsclub.com 134 | ign.com 135 | qvc.com 136 | npr.org 137 | patch.com 138 | dell.com 139 | accuweather.com 140 | vimeo.com 141 | expedia.com 142 | trulia.com 143 | ca.gov 144 | swagbucks.com 145 | spotify.com 146 | bedbathandbeyond.com 147 | nypost.com 148 | aliexpress.com 149 | blackboard.com 150 | ticketmaster.com 151 | ikea.com 152 | feedly.com 153 | usaa.com 154 | tmz.com 155 | quora.com 156 | lifehacker.com 157 | kayak.com 158 | reference.com 159 | zappos.com 160 | gizmodo.com 161 | slate.com 162 | faithtap.com 163 | adp.com 164 | abcnews.go.com 165 | sephora.com 166 | cbs.com 167 | latimes.com 168 | shutterfly.com 169 | t-mobile.com 170 | littlethings.com 171 | glassdoor.com 172 | bloomberg.com 173 | cbsnews.com 174 | wikihow.com 175 | walgreens.com 176 | usbank.com 177 | blogger.com 178 | weebly.com 179 | gamestop.com 180 | food.com 181 | time.com 182 | kickstarter.com 183 | okcupid.com 184 | aa.com 185 | weather.gov 186 | nametests.com 187 | fandango.com 188 | engadget.com 189 | steamcommunity.com 190 | thekitchn.com 191 | nba.com 192 | mashable.com 193 | hp.com 194 | gamefaqs.com 195 | delta.com 196 | breitbart.com 197 | coupons.com 198 | eonline.com 199 | surveymonkey.com 200 | kmart.com 201 | barnesandnoble.com 202 | meetup.com 203 | bhphotovideo.com 204 | fanduel.com 205 | quizlet.com 206 | nydailynews.com 207 | sbnation.com 208 | nbcsports.com 209 | likes.com 210 | bbc.co.uk 211 | ew.com 212 | nike.com 213 | rottentomatoes.com 214 | steampowered.com 215 | reuters.com 216 | qq.com 217 | today.com 218 | mapquest.com 219 | audible.com 220 | priceline.com 221 | whitepages.com 222 | united.com 223 | myfitnesspal.com 224 | icloud.com 225 | forever21.com 226 | theatlantic.com 227 | microsoftstore.com 228 | theverge.com 229 | gawker.com 230 | houzz.com 231 | mayoclinic.org 232 | rei.com 233 | sfgate.com 234 | lifebuzz.com 235 | discover.com 236 | pnc.com 237 | pof.com 238 | iflscience.com 239 | popsugar.com 240 | creditkarma.com 241 | telegraph.co.uk 242 | airbnb.com 243 | buzzlie.com 244 | cnbc.com 245 | deadspin.com 246 | sina.com.cn 247 | legacy.com 248 | thedailybeast.com 249 | samsung.com 250 | nextdoor.com 251 | evite.com 252 | shopify.com 253 | yellowpages.com 254 | pcmag.com 255 | redfin.com 256 | emgn.com 257 | weibo.com 258 | alibaba.com 259 | cabelas.com 260 | battle.net 261 | foxsports.com 262 | taobao.com 263 | eventbrite.com 264 | victoriassecret.com 265 | theblaze.com 266 | dealnews.com 267 | cbslocal.com 268 | cvs.com 269 | dailymotion.com 270 | ecollege.com 271 | gofundme.com 272 | fitbit.com 273 | instructables.com 274 | godaddy.com 275 | babycenter.com 276 | squarespace.com 277 | llbean.com 278 | dickssportinggoods.com 279 | 6pm.com 280 | myway.com 281 | hsn.com 282 | wired.com 283 | officedepot.com 284 | ozztube.com 285 | usmagazine.com 286 | match.com 287 | cracked.com 288 | evernote.com 289 | box.com 290 | starbucks.com 291 | kbb.com 292 | mlb.com 293 | marriott.com 294 | si.com 295 | jezebel.com 296 | pbs.org 297 | consumerreports.org 298 | roblox.com 299 | urbandictionary.com 300 | kotaku.com 301 | xbox.com 302 | marketwatch.com 303 | refinery29.com 304 | wikimedia.org 305 | tvguide.com 306 | politico.com 307 | barclaycardus.com 308 | abc.go.com 309 | mint.com 310 | topix.com 311 | theblackfriday.com 312 | aarp.org 313 | hotnewhiphop.com 314 | yourdailydish.com 315 | sprint.com 316 | vox.com 317 | cafemom.com 318 | nbc.com 319 | dailykos.com 320 | azlyrics.com 321 | autotrader.com 322 | hilton.com 323 | irs.gov 324 | monster.com 325 | fatwallet.com 326 | mailchimp.com 327 | webex.com 328 | landsend.com 329 | wix.com 330 | usnews.com 331 | jcrew.com 332 | jet.com 333 | capitalone360.com 334 | sharepoint.com 335 | schwab.com 336 | ulta.com 337 | vistaprint.com 338 | rollingstone.com 339 | biblegateway.com 340 | gamespot.com 341 | io9.com 342 | opentable.com 343 | hm.com 344 | duckduckgo.com 345 | chron.com 346 | photobucket.com 347 | shareasale.com 348 | directv.com 349 | avg.com 350 | oracle.com 351 | hotels.com 352 | timewarnercable.com 353 | chicagotribune.com 354 | ehow.com 355 | primewire.ag 356 | abs-cbnnews.com 357 | salon.com 358 | greatergood.com 359 | epicurious.com 360 | fool.com 361 | patheos.com 362 | custhelp.com 363 | purdue.edu 364 | tickld.com 365 | frys.com 366 | indiatimes.com 367 | amazon.co.uk 368 | zendesk.com 369 | tigerdirect.com 370 | stubhub.com 371 | healthcare.gov 372 | archive.org 373 | qualtrics.com 374 | ravelry.com 375 | cars.com 376 | redbox.com 377 | jalopnik.com 378 | speedtest.net 379 | harvard.edu 380 | slideshare.net 381 | kinja.com 382 | nesn.com 383 | michaels.com 384 | mit.edu 385 | bodybuilding.com 386 | edmunds.com 387 | nhl.com 388 | zergnet.com 389 | terraclicks.com 390 | techcrunch.com 391 | regnok.com 392 | pogo.com 393 | backpage.com 394 | mozilla.org 395 | naver.com 396 | giphy.com 397 | bankrate.com 398 | msnbc.com 399 | digitaltrends.com 400 | fanfiction.net 401 | skype.com 402 | disney.go.com 403 | norton.com 404 | androidcentral.com 405 | tomshardware.com 406 | thefreedictionary.com 407 | liveleak.com 408 | 247sports.com 409 | merriam-webster.com 410 | wnd.com 411 | earthlink.net 412 | conservativetribune.com 413 | independent.co.uk 414 | drugs.com 415 | rotoworld.com 416 | nationalgeographic.com 417 | ae.com 418 | noaa.gov 419 | arstechnica.com 420 | thinkgeek.com 421 | stanford.edu 422 | bizjournals.com 423 | hootsuite.com 424 | genius.com 425 | goodhousekeeping.com 426 | vanguard.com 427 | ny.gov 428 | citibankonline.com 429 | booking.com 430 | mic.com 431 | orbitz.com 432 | dominos.com 433 | medium.com 434 | wow.com 435 | urbanoutfitters.com 436 | douban.com 437 | timeanddate.com 438 | draftkings.com 439 | livestrong.com 440 | livingsocial.com 441 | cox.net 442 | theonion.com 443 | marthastewart.com 444 | comenity.net 445 | worldlifestyle.com 446 | disney.com 447 | realsimple.com 448 | vrbo.com 449 | playstation.com 450 | potterybarn.com 451 | zazzle.com 452 | ksl.com 453 | tdbank.com 454 | sourceforge.net 455 | careerbuilder.com 456 | -------------------------------------------------------------------------------- /RadiumBrowser/tracker.json: -------------------------------------------------------------------------------- 1 | [{"trigger":{"url-filter":".*","if-domain":["adjust.io","airbrake.io","appboy.com","appsflyer.com","apsalar.com","bango.combango.org","bango.net","basic-check.disconnect.me","bkrtx.com","bluekai.com","bugsense.com","burstly.com","chartboost.com","count.ly","crashlytics.com","crittercism.com","custom-blacklisted-tracking-example.com","do-not-tracker.org","eviltracker.net","flurry.com","getexceptional.com","inmobi.com","jumptap.com","localytics.com","mixpanel.com","mobile-collector.newrelic.com","mobileapptracking.com","playtomic.com","stathat.com","supercell.net","tapjoy.com","trackersimulator.org","usergrid.com","vungle.com"]},"action":{"type":"block"}}] 2 | -------------------------------------------------------------------------------- /RadiumBrowser/zeus.json: -------------------------------------------------------------------------------- 1 | [{"trigger":{"url-filter":".*","if-domain":["039b1ee.netsolhost.com","03a6b7a.netsolhost.com","03a6f57.netsolhost.com","03bbec4.netsolhost.com","0if1nl6.org","0x.x.gg","54g35546-5g6hbggffhb.tk","76tguy6hh6tgftrt7tg.su","afobal.cl","ahmedashid.com","aljazeera.kz","allfortune777.biz","alvoportas.com.br","analiticwebexperience.com","anderlechti.com","angryshippflyforok.su","apple-trusted.com","arcelikpendikservisi.gen.tr","arvision.com.co","ashoesheestono.eu","atmape.ru","auto-fx.fr","avast-mail-security.download","axpoium.echange.su","az-armaturen.su","b.1s2.in.ua","badlywantyou.top","baliwag.xyz","banana.dp.ua","baoshlda.com","beatyhousesupporte.su","bellinghambar.tk","bestboy.top","bestdove.in.ua","bestframingnailerreview.com","bibrath.eu","bikeshophaidli.com","bin1.kns1.al","bitters.su","bl1nqz8yrf7tgdsq.tk","blackhill.pp.ua","blog.raw-recruits.com","blogerjijer.pw","bolerakopsoa.pw","bora.studentworkbook.pw","bots.configbinbots.info","branchtist.com","brausincsystem.pro","bright.su","brothersmt2.tk","burgerspendingbusiness.kz","burn.settingsdata.store","burrinsurance.com","bytes.darktech.org","canadianonlineagreementservices.kz","capacitacion.inami.gob.mx","casher777soft.pw","cd31411.tmweb.ru","chambercb.tk","championbft.com","chezhiyasweheropasl.su","chhathpuja.com","ciceidr.top","cicero-dropbox.tk","circleread-view.com.mocha2003.mochahost.com","citricbenz.website","code.blaztech.gdn","codebacktowork2.tk","corefwdgroup.tk","cosmosdady.su","counter-1.adscounter.com.ua","cp53072.cloudhosting.lv","cp53091.cloudhosting.lv","cryptmyexe.pw","cscomnsinc.tk","cupomkinghost.com.br","cy-m0ld.com","cynthialemos1225.ddns.net","daehoshipinc.tk","danislenefc.info","dasch.pl ","dattinggate.com","dau43vt5wtrd.tk","dbimbem.axfree.com","ddobnajanu.club","dealing.com.ng","debservers.pw","dejavu-now.tk","delaponitan.pw","depolakoeasre.pw","dereksing.top","deserado.tk","developer.cdn.com.kz","difusoragoiania.com.br","diguing-store.net","dileconme.hotmail.ru","dino1.ddf.al","dlauten.bplaced.net","domifondery.com","domifondery3d.com","dominoziele.pw","domnicpeter.in.net","doratopelase.pw","dzitech.net","eavgwy5suy.tk","ebesee.com","eite.asia","emaillifecoaching.com.au","exploit0day.top","extremesports.kz","f8b2b9.su","fadzulani.com","felanco.heliohost.org","felceconserve.com","fenipourashava.com","fileserver03.com","finalcrashtest.co.nz","finsolutions.top","flex.comonwealthplc.com","fludgwererqo.at","fmarchzeetequwxtw.wang","foxlimited.top","foxmanwer.pw","frank1.ddf.al","franka.in.net","fretiolo.com","frevolore.com","fx45.pp.ru","gate.timstackleshop.es","genteatsss.com","gmailsecurityteam.com","go.everli-killz.xyz","goodbytoname.com","google.poultrymiddleeast.com","googlepetkavanis4.pw","grupocava-mx.com","gskpresident.tk","gyalkingerz.com","gyodundena.hotmail.ru","gzhueyuatex.com","hanocomin.com","hdfc.pp.ru","henex.net.ua","hillalala.com","holydoome.co.uk","homedeco.com.bo","honestme.com.ua","hotelavalon.org","hruner.com","hui-ain-apparel.tk","hyperbolic.tk","ice.andromed.in.ua","ice.ip64.net","iclear.studentworkbook.pw","igor32.herbalbrasil.com.br","ijoe.xyz","illinoisnets.net","iltempo.com.au","imamnhearte.hotmail.ru","immaculate.tk","ingenicopads.kz","interlogistics.com.vn","islenpiding.hotmail.ru","istyle.ge","ivansaru.418.com1.ru","j0k3rj0k3r.tk","jaaphram.com","jacoblanderville.myjino.ru","jad.fisbonline.com","jangasm.org","japanparts.pw","jayboyd.t15.org","jazmany.cu.ma","jgworldupd.com","joejdbjrmrkklfnmf.usr.me","joepussy.tk","johnluis33.tk","jomo.in.ua","joyclasses.eu","juanadearco.com.uy","jump1ng.net","junllian.net","junniper.mcdir.ru","jutebags.tk","jutrack.dp.ua","karma-bodrum.com","katagi-weblogs.lolipop.jp","kaydayeuti.axfree.com","kesikelyaf.com","kjkdndskjl.info","kntksales.tk","krestenbv.nl","kudrnwosas.faith","kw34h-lithi-owo.tk","l3d.pp.ru","l3d1.pp.ru","leo94dhgfyw-df87fb.tk","leon10.5gbfree.com","lifeisgoodwhenu2.info","lifestyles.pp.ru","links.heliohost.org","lion.web2.0campus.net","lisovfoxcom.418.com1.ru","littwronthath.net","liuz112.ddns.net","liveresellerweb.eu","livinglounges.su","luenhinpearl.com","lurdinha.psc.br","lzediamike.trade","machine.cu.ma","mailslots.top","malikshabas.com","maminoleinc.tk","man-street.tk","matt001.tk","me.centronind.club","megastats.top","metalexvietnamreed.tk","micheal766.info","mm266.bplaced.com","molowo.in.ua","monsierdroider.wang","movieofgoodies.kz","moviepaidinfullsexy.kz","mycraft.com.br","myfcb.tk","mygoodness.in.ua","mymytonnymaxltd.org","naaninggeschcho.hotmail.ru","naijabids.co.uk","nancycemt1225.ddns.net","nasscomminc.tk","natlalirans.hotmail.ru","neorandom.dothome.co.kr","nettech.org.in","newversionpdun.in.net","nicktung.com","nonstopeddanceraz.su","noonepa.tk","ns416017.ip-37-187-144.eu","ns511849.ip-192-99-19.net","ns513726.ip-192-99-148.net","nsdic.pp.ru","obyno.xyz","ozowarac.com","p-alpha.ooo.al","pandyi.com","panel.vargakragard.se","pedropedreiromoxik.su","pfengineering.com","pharirgatic.hotmail.ru","pharmaspan.com","phimsex4u.biz","platinum-casino.ru","plvk-power.com","police-fbi.securityservice.review","polyseed.my","porschecosv.com","portal.100am100.kz","preapprovedloansoffline.kz","preapprovedloansonline.kz","presleywebs.uk.pn","prk.firstconf.3gb.biz","programtotalatoma.esy.es","projects.globaltronics.net","prtscrinsertcn.net","qbhcsope.com","rarabarnfi.hotmail.ru","regame.su","registerdrivegoogle.sytes.net","regsways.top","reimbergit.com.br","reserve.jumpingcrab.com","resr.configure.8c1.net","rexafajay.axfree.com","rhwndkf45.codns.com","ricasad.sx","roofgreen.in.ua","rsslessons.su","ryuitaqw.pw","s1.eyeonmusica.it","sameetc.tk","samoniklo.pw","sandokan66.no-ip.info","sanyai-love.rmu.ac.th","sdhfjksdhfjksdh.biz.ua","sdspropro.co.ua","secufast.bplaced.net","secure.lynxbowlingservices.com","securetalk.cwsurf.de","securetestingnetwotk.com","server.bovine-mena.com","serverjainpangwang.pw","serversss.biz","servmill.com","shadowraze.pw","shaktitextileengr.com","slanted.excelonlinereader.services","slap.alliancekl.com","slivoratikam.pw","slot.sub-zero.it","smartfoodsglutenfree.kz","solubaba.tk","spartanr.5gbfree.com","speroni.pw","spotmarka.ap0x.com","ssl.sinergycosmetics.com","sslsam.com","stats.lead.mysitehosted.com","storroliko.club","sub.beirinckx.be","suryapolix.club","sus.nieuwmoer.info","sv1.eyeonmusica.it","svitor.hostev.net","systemhelpr.com","teachgalaxys.website","tech-dan.xyz","techakym.pw","techemeka.work","techmag.space","techmaha.pw","teeth.co.jp","tekadrian.pro","tekadrian.xyz","tekcharles.xyz","tekchuks.xyz","tekjoe.space","teksoft.pro","telefonfiyatlari.org","tesab.org.uk","tradecharm.lt","traptrillhosts.top","treatstartaugusth.info","tronuprising.heliohost.org","trust-s-b.com","tt.onmypc.org","turkeyhotelnoslafas.su","u0003321.cp.regruhosting.ru","u8781a21.pw","ugwebz.uk.pn","update.odeen.eu","update.rifugiopontese.it","update.saintfrancoisath.be","update.timstackleshop.es","updateacces.org","updating-flash.cloudapp.net","uptight.su","valdmir.noriysha.ru","valntooglesakrundigk.pw","vankapetkavanis4.pw","vashadvokat.in.ua","vassabgg.pw","vegantravelshow.com","vinking.top","visit2013.in.ua","vodahelp.sytes.net","warriorinjapan.hostjava.net","wasabi.mine.nu","waserazer.pw","weporsche.com","winhelptech.xyz","wor6.b6dfnahea.ns2.name","wvin.su","www.02level.tk","www.anothersideofpeace.org","www.antibasic.ga","www.basecinco.com.ar","www.bilbobaggins.comxa.com","www.cpro.moscow","www.impm.upel.edu.ve","www.ipm.upel.edu.ve","www.jobdeliver.tk","www.jung201.tk","www.mauritaniecoeur.org","www.mikewine.tk","www.nikey.cn","www.ohimmades.pw","www.orquestanacaona.cult.cu","www.perilshed.info","www.personxing.in.net","www.poloatmer.ru","www.rimmugygur.is","www.riverwalktrader.co.za","www.sdspropro.co.ua","www.slapintins.publicvm.com","www.sunbrehden.top","www.tekyalhaja.xyz","www.witkey.com","wypratama.co.id","xbsezlmaha.loan","xclones.in.net","xpipemotoring.top","ya-aaaa123123.myjino.ru","yamleg.fu8.com","z0bu.dynu.com","z3us1.z-ed.info","zabava-bel.ru","zadmj.cp-addltives.com","zetes.vdsinside.com"]},"action":{"type":"block"}}] 2 | -------------------------------------------------------------------------------- /RadiumBrowserTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /RadiumBrowserTests/RadiumBrowserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadiumBrowserTests.swift 3 | // RadiumBrowserTests 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import RadiumBrowser 11 | 12 | class RadiumBrowserTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /RadiumBrowserUITests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /RadiumBrowserUITests/RadiumBrowserUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadiumBrowserUITests.swift 3 | // RadiumBrowserUITests 4 | // 5 | // Created by bslayter on 1/31/17. 6 | // Copyright © 2017 bslayter. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class RadiumBrowserUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | } 23 | 24 | override func tearDown() { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | super.tearDown() 27 | } 28 | 29 | func testExample() { 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier "com.SlayterDevelopment.RadiumBrowser" # The bundle identifier of your app 2 | apple_id "slayteritunes@gmail.com" # Your Apple email address 3 | 4 | team_id "Z67LVNYQSE" # Developer Portal Team ID 5 | 6 | # you can even provide different app identifiers, Apple IDs and team names per lane: 7 | # More information: https://docs.fastlane.tools/advanced/#appfile 8 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # Customize this file, documentation can be found here: 2 | # https://docs.fastlane.tools/actions/ 3 | # All available actions: https://docs.fastlane.tools/actions 4 | # can also be listed using the `fastlane actions` command 5 | 6 | # Change the syntax highlighting to Ruby 7 | # All lines starting with a # are ignored when running `fastlane` 8 | 9 | # If you want to automatically update fastlane if a new version is available: 10 | # update_fastlane 11 | 12 | # This is the minimum version number required. 13 | # Update this, if you use features of a newer version 14 | fastlane_version "2.64.0" 15 | 16 | default_platform :ios 17 | 18 | platform :ios do 19 | before_all do 20 | # ENV["SLACK_URL"] = "https://hooks.slack.com/services/..." 21 | cocoapods 22 | # carthage 23 | end 24 | 25 | lane :refresh_dsyms do 26 | download_dsyms # Download dSYM files from iTC 27 | upload_symbols_to_crashlytics # Upload them to Crashlytics 28 | clean_build_artifacts # Delete the local dSYM files 29 | end 30 | 31 | desc "Runs all the tests" 32 | lane :test do 33 | scan 34 | end 35 | 36 | desc "Submit a new Beta Build to Apple TestFlight" 37 | desc "This will also make sure the profile is up to date" 38 | lane :beta do 39 | # match(type: "appstore") # more information: https://codesigning.guide 40 | gym(scheme: "RadiumBrowser") # Build your app - more options available 41 | pilot 42 | 43 | # sh "your_script.sh" 44 | # You can also use other beta testing services here (run `fastlane actions`) 45 | end 46 | 47 | desc "Deploy a new version to the App Store" 48 | lane :release do 49 | # match(type: "appstore") 50 | # snapshot 51 | gym(scheme: "RadiumBrowser") # Build your app - more options available 52 | deliver(force: true) 53 | # frameit 54 | end 55 | 56 | # You can define as many lanes as you want 57 | 58 | after_all do |lane| 59 | # This block is called, only if the executed lane was successful 60 | 61 | # slack( 62 | # message: "Successfully deployed new App Update." 63 | # ) 64 | end 65 | 66 | error do |lane, exception| 67 | # slack( 68 | # message: exception.message, 69 | # success: false 70 | # ) 71 | end 72 | end 73 | 74 | 75 | # More information about multiple platforms in fastlane: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Platforms.md 76 | # All available actions: https://docs.fastlane.tools/actions 77 | 78 | # fastlane reports which actions are used. No personal data is recorded. 79 | # Learn more at https://docs.fastlane.tools/#metrics 80 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | ## Choose your installation method: 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
HomebrewInstaller ScriptRubyGems
macOSmacOSmacOS or Linux with Ruby 2.0.0 or above
brew cask install fastlaneDownload the zip file. Then double click on the install script (or run it in a terminal window).sudo gem install fastlane -NV
30 | 31 | # Available Actions 32 | ## iOS 33 | ### ios refresh_dsyms 34 | ``` 35 | fastlane ios refresh_dsyms 36 | ``` 37 | 38 | ### ios test 39 | ``` 40 | fastlane ios test 41 | ``` 42 | Runs all the tests 43 | ### ios beta 44 | ``` 45 | fastlane ios beta 46 | ``` 47 | Submit a new Beta Build to Apple TestFlight 48 | 49 | This will also make sure the profile is up to date 50 | ### ios release 51 | ``` 52 | fastlane ios release 53 | ``` 54 | Deploy a new version to the App Store 55 | 56 | ---- 57 | 58 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 59 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 60 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 61 | --------------------------------------------------------------------------------