VPN v2
This commit is contained in:
57
main/Extensions/AlertSheet.swift
Normal file
57
main/Extensions/AlertSheet.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: Basic Alerts
|
||||
|
||||
func Alert(title: String?, text: String?, buttonText: String = "Dismiss") -> UIAlertController {
|
||||
let alert = UIAlertController(title: title, message: text, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: buttonText, style: .cancel, handler: nil))
|
||||
return alert
|
||||
}
|
||||
|
||||
func ErrorAlert(_ error: Error, buttonText: String = "Dismiss") -> UIAlertController {
|
||||
return Alert(title: "Error", text: error.localizedDescription, buttonText: buttonText)
|
||||
}
|
||||
|
||||
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping () -> Void) -> UIAlertController {
|
||||
let alert = Alert(title: title, text: text, buttonText: "Cancel")
|
||||
alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action() })
|
||||
return alert
|
||||
}
|
||||
|
||||
extension UIAlertController {
|
||||
func presentIn(_ viewController: UIViewController?) {
|
||||
viewController?.present(self, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Alert with multiple options
|
||||
|
||||
func AlertWithOptions(title: String?, text: String?, buttons: [String], lastIsDestructive: Bool = false, callback: @escaping (_ index: Int?) -> Void) -> UIAlertController {
|
||||
let alert = UIAlertController(title: title, message: text, preferredStyle: .actionSheet)
|
||||
for (i, btn) in buttons.enumerated() {
|
||||
let dangerous = (lastIsDestructive && i + 1 == buttons.count)
|
||||
alert.addAction(UIAlertAction(title: btn, style: dangerous ? .destructive : .default) { _ in callback(i) })
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in callback(nil) })
|
||||
return alert
|
||||
}
|
||||
|
||||
func AlertDeleteLogs(_ domain: String, latest: Timestamp, success: @escaping (_ tsMin: Timestamp) -> Void) -> UIAlertController {
|
||||
let sinceNow = TimestampNow() - latest
|
||||
var buttons = ["Last 5 minutes", "Last 15 minutes", "Last hour", "Last 24 hours", "Delete everything"]
|
||||
var times: [Timestamp] = [300, 900, 3600, 86400]
|
||||
while times.count > 0, times[0] < sinceNow {
|
||||
buttons.removeFirst()
|
||||
times.removeFirst()
|
||||
}
|
||||
return AlertWithOptions(title: "Delete logs", text: "Delete logs for domain '\(domain)'", buttons: buttons, lastIsDestructive: true) {
|
||||
guard let idx = $0 else {
|
||||
return
|
||||
}
|
||||
if idx >= times.count {
|
||||
success(0)
|
||||
} else {
|
||||
success(Timestamp(Date().timeIntervalSince1970) - times[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
91
main/Extensions/Generic.swift
Normal file
91
main/Extensions/Generic.swift
Normal file
@@ -0,0 +1,91 @@
|
||||
import Foundation
|
||||
|
||||
struct QLog {
|
||||
private init() {}
|
||||
static func m(_ message: String) { write("", message) }
|
||||
static func Info(_ message: String) { write("[INFO] ", message) }
|
||||
#if DEBUG
|
||||
static func Debug(_ message: String) { write("[DEBUG] ", message) }
|
||||
#else
|
||||
static func Debug(_ _: String) {}
|
||||
#endif
|
||||
static func Error(_ message: String) { write("[ERROR] ", message) }
|
||||
static func Warning(_ message: String) { write("[WARN] ", message) }
|
||||
private static func write(_ tag: String, _ message: String) {
|
||||
print(String(format: "%1.3f %@%@", Date().timeIntervalSince1970, tag, message))
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection {
|
||||
subscript(ifExist i: Index?) -> Iterator.Element? {
|
||||
guard let i = i else { return nil }
|
||||
return indices.contains(i) ? self[i] : nil
|
||||
}
|
||||
}
|
||||
|
||||
var listOfSLDs: [String : [String : Bool]] = {
|
||||
let path = Bundle.main.url(forResource: "third-level", withExtension: "txt")
|
||||
let content = try! String(contentsOf: path!)
|
||||
var res: [String : [String : Bool]] = [:]
|
||||
content.enumerateLines { line, _ in
|
||||
let dom = line.split(separator: ".")
|
||||
let tld = String(dom.first!)
|
||||
let sld = String(dom.last!)
|
||||
if res[tld] == nil { res[tld] = [:] }
|
||||
res[tld]![sld] = true
|
||||
}
|
||||
return res
|
||||
}()
|
||||
|
||||
extension String {
|
||||
/// Check if string is equal to `domain` or ends with `.domain`
|
||||
func isSubdomain(of domain: String) -> Bool { self == domain || self.hasSuffix("." + domain) }
|
||||
/// Split string into top level domain part and host part
|
||||
func splitDomainAndHost() -> (domain: String, host: String?) {
|
||||
let lastChr = last?.asciiValue ?? 0
|
||||
guard lastChr > UInt8(ascii: "9") || lastChr < UInt8(ascii: "0") else { // IP address
|
||||
return (domain: "# IP connection", host: self)
|
||||
}
|
||||
var parts = components(separatedBy: ".")
|
||||
guard let tld = parts.popLast(), let sld = parts.popLast() else {
|
||||
return (domain: self, host: nil) // no subdomains, just plain SLD
|
||||
}
|
||||
var ending = sld + "." + tld
|
||||
if listOfSLDs[tld]?[sld] ?? false, let rld = parts.popLast() {
|
||||
ending = rld + "." + ending
|
||||
}
|
||||
return (domain: ending, host: parts.joined(separator: "."))
|
||||
|
||||
// var allDots = enumerated().compactMap { $1 == "." ? $0 : nil }
|
||||
// let d1 = allDots.popLast() // we dont care about TLD
|
||||
// guard let d2 = allDots.popLast() else {
|
||||
// return (domain: self, host: nil) // no subdomains, just plain SLD
|
||||
// }
|
||||
// // TODO: check third level domains
|
||||
//// let d3 = allDots.popLast()
|
||||
// return (String(suffix(count - d2 - 1)), String(prefix(d2)))
|
||||
}
|
||||
}
|
||||
|
||||
extension Timer {
|
||||
@discardableResult static func repeating(_ interval: TimeInterval, call selector: Selector, on target: Any, userInfo: Any? = nil) -> Timer {
|
||||
Timer.scheduledTimer(timeInterval: interval, target: target, selector: selector,
|
||||
userInfo: userInfo, repeats: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension DateFormatter {
|
||||
convenience init(withFormat: String) {
|
||||
self.init()
|
||||
dateFormat = withFormat
|
||||
}
|
||||
func with(format: String) -> Self {
|
||||
dateFormat = format
|
||||
return self
|
||||
}
|
||||
func string(from ts: Timestamp) -> String {
|
||||
string(from: Date.init(timeIntervalSince1970: Double(ts)))
|
||||
}
|
||||
}
|
||||
|
||||
func TimestampNow() -> Timestamp { Timestamp(Date().timeIntervalSince1970) }
|
||||
20
main/Extensions/GroupedDomain.swift
Normal file
20
main/Extensions/GroupedDomain.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
|
||||
extension GroupedDomain {
|
||||
static func +(a: GroupedDomain, b: GroupedDomain) -> Self {
|
||||
GroupedDomain(domain: a.domain, total: a.total + b.total, blocked: a.blocked + b.blocked,
|
||||
lastModified: max(a.lastModified, b.lastModified), options: a.options ?? b.options )
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == GroupedDomain {
|
||||
func merge(_ domain: String, options opt: FilterOptions? = nil) -> GroupedDomain {
|
||||
var b: Int32 = 0, t: Int32 = 0, m: Timestamp = 0
|
||||
for x in self {
|
||||
b += x.blocked
|
||||
t += x.total
|
||||
m = Swift.max(m, x.lastModified)
|
||||
}
|
||||
return GroupedDomain(domain: domain, total: t, blocked: b, lastModified: m, options: opt)
|
||||
}
|
||||
}
|
||||
23
main/Extensions/Notifications.swift
Normal file
23
main/Extensions/Notifications.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
|
||||
let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // VPNState!
|
||||
let NotifyFilterChanged = NSNotification.Name("PSIFilterSettingsChanged") // nil!
|
||||
let NotifyLogHistoryReset = NSNotification.Name("PSILogHistoryReset") // nil!
|
||||
|
||||
|
||||
extension NSNotification.Name {
|
||||
func post(_ obj: Any? = nil) {
|
||||
NotificationCenter.default.post(name: self, object: obj)
|
||||
}
|
||||
func postOnMainThread(_ obj: Any? = nil) {
|
||||
DispatchQueue.main.async { NotificationCenter.default.post(name: self, object: obj) }
|
||||
}
|
||||
/// You are responsible for removing the returned object in a `deinit` block.
|
||||
// @discardableResult func observe(queue: OperationQueue? = nil, using block: @escaping (Notification) -> Void) -> NSObjectProtocol {
|
||||
// NotificationCenter.default.addObserver(forName: self, object: nil, queue: queue, using: block)
|
||||
// }
|
||||
/// On iOS 9.0+ you don't need to unregister the observer.
|
||||
func observe(call: Selector, on target: Any, obj: Any? = nil) {
|
||||
NotificationCenter.default.addObserver(target, selector: call, name: self, object: obj)
|
||||
}
|
||||
}
|
||||
8
main/Extensions/SharedState.swift
Normal file
8
main/Extensions/SharedState.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
let dateTimeFormat = DateFormatter(withFormat: "yyyy-MM-dd HH:mm:ss")
|
||||
var currentVPNState: VPNState = .off
|
||||
|
||||
public enum VPNState : Int {
|
||||
case on = 1, inbetween, off
|
||||
}
|
||||
93
main/Extensions/TableView.swift
Normal file
93
main/Extensions/TableView.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
import UIKit
|
||||
|
||||
extension GroupedDomain {
|
||||
var detailCellText: String { get {
|
||||
return blocked > 0
|
||||
? "\(dateTimeFormat.string(from: lastModified)) — \(blocked)/\(total) blocked"
|
||||
: "\(dateTimeFormat.string(from: lastModified)) — \(total)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FilterOptions {
|
||||
func tableRowImage() -> UIImage? {
|
||||
let blocked = contains(.blocked)
|
||||
let ignored = contains(.ignored)
|
||||
if blocked { return UIImage(named: ignored ? "block_ignore" : "shield-x") }
|
||||
if ignored { return UIImage(named: "quicklook-not") }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
func withColor(_ color: UIColor, fromBack: Int) -> Self {
|
||||
let l = length - fromBack
|
||||
let r = (l < 0) ? NSMakeRange(0, length) : NSMakeRange(l, fromBack)
|
||||
self.addAttribute(.foregroundColor, value: color, range: r)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Pull-to-Refresh
|
||||
|
||||
extension UIRefreshControl {
|
||||
convenience init(call: Selector, on: UITableViewController) {
|
||||
self.init()
|
||||
addTarget(on, action: call, for: .valueChanged)
|
||||
addTarget(self, action: #selector(endRefreshing), for: .valueChanged)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Incremental Update Delegate
|
||||
|
||||
protocol IncrementalDataSourceUpdate : UITableViewController {
|
||||
var dataSource: [GroupedDomain] { get set }
|
||||
}
|
||||
|
||||
extension IncrementalDataSourceUpdate {
|
||||
func ifDisplayed(_ block: () -> Void) {
|
||||
DispatchQueue.main.sync {
|
||||
if self.tableView.window?.isKeyWindow ?? false {
|
||||
block()
|
||||
// TODO: custom handling if cell is being edited
|
||||
} else {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
func insertRow(_ obj: GroupedDomain, at index: Int) {
|
||||
dataSource.insert(obj, at: index)
|
||||
ifDisplayed {
|
||||
self.tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .left)
|
||||
}
|
||||
}
|
||||
func moveRow(_ obj: GroupedDomain, from: Int, to: Int) {
|
||||
dataSource.remove(at: from)
|
||||
dataSource.insert(obj, at: to)
|
||||
ifDisplayed {
|
||||
let source = IndexPath(row: from, section: 0)
|
||||
let cell = self.tableView.cellForRow(at: source)
|
||||
cell?.detailTextLabel?.text = obj.detailCellText
|
||||
self.tableView.moveRow(at: source, to: IndexPath(row: to, section: 0))
|
||||
}
|
||||
}
|
||||
func replaceRow(_ obj: GroupedDomain, at index: Int) {
|
||||
dataSource[index] = obj
|
||||
ifDisplayed {
|
||||
self.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
}
|
||||
}
|
||||
func deleteRow(at index: Int) {
|
||||
dataSource.remove(at: index)
|
||||
ifDisplayed {
|
||||
self.tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
}
|
||||
}
|
||||
func replaceData(with newData: [GroupedDomain]) {
|
||||
dataSource = newData
|
||||
ifDisplayed {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user