-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
38 changed files
with
1,109 additions
and
1,450 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// | ||
// AppDelegate.swift | ||
// Online | ||
// | ||
// Created by syan on 13/04/2024. | ||
// Copyright © 2024 Syan. All rights reserved. | ||
// | ||
|
||
import Cocoa | ||
import SYProxy | ||
|
||
@main | ||
class AppDelegate: NSObject, NSApplicationDelegate { | ||
|
||
// MARK: Properties | ||
@IBOutlet private var statusMenu: NSMenu! | ||
private var statusItem: NSStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) | ||
private var popover: NSPopover? | ||
|
||
// MARK: AppDelegate | ||
func applicationDidFinishLaunching(_ notification: Notification) { | ||
statusMenu.delegate = self | ||
statusItem.menu = statusMenu | ||
statusItem.image = WebsiteStatus.unknown.image | ||
statusItem.highlightMode = true | ||
|
||
Storage.shared.delegate = self | ||
|
||
Crawler.shared.websites = Storage.shared.websites | ||
Crawler.shared.delegate = self | ||
|
||
ProxyURLProtocol.register() | ||
ProxyURLProtocol.isLoggingEnabled = true | ||
ProxyURLProtocol.dataSource = self | ||
} | ||
|
||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { | ||
return false | ||
} | ||
|
||
// MARK: Images | ||
private func updateIcon() { | ||
let statuses = Array(Crawler.shared.statuses.values) | ||
let prominentStatus = statuses.first(where: \.isError) ?? statuses.first(where: \.isTimeout) ?? statuses.first(where: \.isOn) ?? WebsiteStatus.unknown | ||
statusItem.image = prominentStatus.image | ||
statusItem.menu?.items.forEach { item in | ||
if let websiteID = item.representedObject as? String { | ||
let status = Crawler.shared.statuses[websiteID] ?? .unknown | ||
item.image = status.image | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension AppDelegate { | ||
private func openForm(for website: Website?) { | ||
let vc = FormViewController.viewController(website: website) | ||
openPopover(for: vc) | ||
} | ||
|
||
private func openSettings() { | ||
let vc = SettingsViewController.nibViewController() | ||
openPopover(for: vc) | ||
} | ||
|
||
private func openPopover(for viewController: NSViewController) { | ||
popover?.close() | ||
|
||
popover = NSPopover() | ||
popover?.behavior = .transient | ||
popover?.contentViewController = viewController | ||
popover?.animates = false | ||
popover?.show(relativeTo: statusItem.button!.frame, of: statusItem.button!, preferredEdge: .minY) | ||
popover?.contentViewController?.view.window?.makeKeyAndOrderFront(NSApp) | ||
NSApp.activate(ignoringOtherApps: true) | ||
} | ||
} | ||
|
||
extension AppDelegate: StorageDelegate { | ||
func storageColorsChanged(_ storage: Storage) { | ||
updateIcon() | ||
} | ||
|
||
func storageWebsitesChanged(_ storage: Storage) { | ||
Crawler.shared.websites = storage.websites | ||
} | ||
} | ||
|
||
extension AppDelegate: CrawlerDelegate { | ||
func crawlerResultsChanged(_ crawler: Crawler) { | ||
updateIcon() | ||
} | ||
} | ||
|
||
extension AppDelegate: ProxyURLProtocolDataSource { | ||
func proxyURLProtocolRequiresFirstProxyMatching(url: URL) -> Proxy? { | ||
return Storage.shared.proxies.first(matching: url) | ||
} | ||
} | ||
|
||
extension AppDelegate: NSMenuDelegate { | ||
func menuWillOpen(_ menu: NSMenu) { | ||
popover?.close() | ||
} | ||
|
||
func menuNeedsUpdate(_ menu: NSMenu) { | ||
guard menu == statusMenu else { return } | ||
|
||
menu.removeAllItems() | ||
|
||
for website in Storage.shared.websites { | ||
let status = Crawler.shared.statuses[website.identifier] ?? .unknown | ||
|
||
let item = NSMenuItem(title: website.name, action: nil, keyEquivalent: "") | ||
item.image = status.image | ||
item.representedObject = website.identifier | ||
item.submenu = NSMenu() | ||
menu.addItem(item) | ||
|
||
addMenuItem(title: status.message, to: item.submenu!) | ||
addMenuItem(title: "Edit...", to: item.submenu!) { | ||
self.openForm(for: website) | ||
} | ||
addMenuItem(title: "Delete", to: item.submenu!) { | ||
let alert = NSAlert() | ||
alert.messageText = "Are you sure you want to delete \(website.name)?" | ||
alert.informativeText = "This operation cannot be undone." | ||
alert.addButton(withTitle: "Delete").keyEquivalent = "" | ||
alert.addButton(withTitle: "Cancel").keyEquivalent = "\r" | ||
if alert.runModal() == .alertFirstButtonReturn { | ||
Storage.shared.removeWebsite(website) | ||
} | ||
} | ||
} | ||
|
||
menu.addItem(.separator()) | ||
|
||
addMenuItem(title: "Add a new website...", to: menu) { | ||
self.openForm(for: nil) | ||
} | ||
|
||
addMenuItem(title: "Settings", to: menu) { | ||
self.openSettings() | ||
} | ||
|
||
addMenuItem(title: "Quit", to: menu) { | ||
NSApp.terminate(nil) | ||
} | ||
} | ||
|
||
private func addMenuItem(title: String, to menu: NSMenu, action: (() -> ())? = nil) { | ||
let item = NSMenuItem(title: title, action: #selector(self.menuTapped(item:)), keyEquivalent: "") | ||
item.target = self | ||
item.representedObject = action | ||
menu.addItem(item) | ||
} | ||
|
||
@objc private func menuTapped(item: NSMenuItem) { | ||
let action = item.representedObject as? () -> () | ||
action?() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// | ||
// Crawler.swift | ||
// Online | ||
// | ||
// Created by syan on 13/04/2024. | ||
// Copyright © 2024 Syan. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
protocol CrawlerDelegate: NSObjectProtocol { | ||
func crawlerResultsChanged(_ crawler: Crawler) | ||
} | ||
|
||
class Crawler { | ||
|
||
// MARK: Init | ||
static let shared = Crawler() | ||
private init() { | ||
restartAll() | ||
} | ||
|
||
// MARK: Properties | ||
weak var delegate: CrawlerDelegate? | ||
var websites: [Website] = [] { | ||
didSet { | ||
restartAll() | ||
} | ||
} | ||
private(set) var statuses: [String: WebsiteStatus] = [:] | ||
private var timers: [Timer] = [] | ||
private let urlSession = URLSession(configuration: .default) | ||
|
||
// MARK: Internal methods | ||
private func restartAll() { | ||
timers.forEach { $0.invalidate() } | ||
timers = [] | ||
|
||
websites.forEach { website in | ||
crawl(websiteID: website.identifier) | ||
} | ||
} | ||
|
||
private func crawl(websiteID: String) { | ||
guard let website = websites.first(where: { $0.identifier == websiteID }), let url = URL(string: website.url) else { | ||
statuses[websiteID] = nil | ||
return | ||
} | ||
|
||
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: website.timeout) | ||
let task = urlSession.dataTask(with: request) { data, response, error in | ||
let status = WebsiteStatus(httpStatus: (response as? HTTPURLResponse)?.statusCode, error: error) | ||
self.statuses[websiteID] = status | ||
|
||
let timeBeforeNextUpdate = status.isOn ? website.timeBeforeRetryIfSuccessed : website.timeBeforeRetryIfFailed | ||
let timer = Timer(timeInterval: timeBeforeNextUpdate, target: self, selector: #selector(self.timerTick), userInfo: ["websiteID": websiteID], repeats: false) | ||
RunLoop.main.add(timer, forMode: .common) | ||
self.timers.append(timer) | ||
|
||
DispatchQueue.main.async { | ||
self.delegate?.crawlerResultsChanged(self) | ||
} | ||
} | ||
task.resume() | ||
} | ||
|
||
@objc private func timerTick(_ timer: Timer) { | ||
defer { | ||
timer.invalidate() | ||
timers.removeAll(where: { $0 == timer }) | ||
} | ||
|
||
guard let userInfo = timer.userInfo as? [String: String], let websiteID = userInfo["websiteID"] else { | ||
print("Unknown timer fired, userInfo was: \(timer.userInfo ?? [:])") | ||
return | ||
} | ||
|
||
crawl(websiteID: websiteID) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// | ||
// Storage.swift | ||
// Online | ||
// | ||
// Created by syan on 13/04/2024. | ||
// Copyright © 2024 Syan. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import Cocoa | ||
import SYProxy | ||
|
||
protocol StorageDelegate: NSObjectProtocol { | ||
func storageWebsitesChanged(_ storage: Storage) | ||
func storageColorsChanged(_ storage: Storage) | ||
} | ||
|
||
class Storage { | ||
|
||
// MARK: Init | ||
static let shared = Storage() | ||
private init() { | ||
websites = UserDefaults.standard.nsCodingValues(Website.self, forKey: "websites") ?? [] | ||
} | ||
|
||
// MARK: Properties | ||
weak var delegate: StorageDelegate? | ||
|
||
// MARK: Colors | ||
var colorSuccess: NSColor { | ||
get { UserDefaults.standard.nsCodingValue(NSColor.self, forKey: "colorSuccess") ?? defaultColorSuccess } | ||
set { UserDefaults.standard.set(nsCodingValue: newValue, forKey: "colorSuccess"); delegate?.storageColorsChanged(self) } | ||
} | ||
var colorTimeout: NSColor { | ||
get { UserDefaults.standard.nsCodingValue(NSColor.self, forKey: "colorTimeout") ?? defaultColorTimeout } | ||
set { UserDefaults.standard.set(nsCodingValue: newValue, forKey: "colorTimeout"); delegate?.storageColorsChanged(self) } | ||
} | ||
var colorFailure: NSColor { | ||
get { UserDefaults.standard.nsCodingValue(NSColor.self, forKey: "colorFailure") ?? defaultColorFailure } | ||
set { UserDefaults.standard.set(nsCodingValue: newValue, forKey: "colorFailure"); delegate?.storageColorsChanged(self) } | ||
} | ||
let defaultColorSuccess: NSColor = .init(calibratedRed: 0, green: 0.82, blue: 0.32, alpha: 1) | ||
let defaultColorTimeout: NSColor = .init(calibratedRed: 1, green: 0.75, blue: 0.29, alpha: 1) | ||
let defaultColorFailure: NSColor = .init(calibratedRed: 1, green: 0.34, blue: 0.37, alpha: 1) | ||
|
||
// MARK: Websites | ||
private(set) var websites: [Website] { | ||
didSet { | ||
UserDefaults.standard.set(nsCodingValues: websites, forKey: "websites") | ||
delegate?.storageWebsitesChanged(self) | ||
} | ||
} | ||
|
||
func addWebsite(_ website: Website) { | ||
if let index = websites.firstIndex(where: { $0.identifier == website.identifier }) { | ||
websites.replaceSubrange(index..<(index + 1), with: [website]) | ||
} | ||
else { | ||
websites.append(website) | ||
} | ||
} | ||
|
||
func removeWebsite(_ website: Website) { | ||
websites.removeAll(where: { $0.identifier == website.identifier }) | ||
} | ||
|
||
func website(for id: String) -> Website? { | ||
return websites.first(where: { $0.identifier == id }) | ||
} | ||
|
||
var proxies: [Proxy] { | ||
return websites.map(\.proxy) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// | ||
// NSColor+SY.swift | ||
// Online | ||
// | ||
// Created by syan on 14/04/2024. | ||
// Copyright © 2024 Syan. All rights reserved. | ||
// | ||
|
||
import Cocoa | ||
|
||
extension NSColor { | ||
func darken(by amount: Double) -> NSColor { | ||
return NSColor( | ||
calibratedRed: redComponent - amount, | ||
green: greenComponent - amount, | ||
blue: blueComponent - amount, | ||
alpha: alphaComponent | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// | ||
// NSImage+SY.swift | ||
// Online | ||
// | ||
// Created by syan on 13/04/2024. | ||
// Copyright © 2024 Syan. All rights reserved. | ||
// | ||
|
||
import Cocoa | ||
|
||
extension NSImage { | ||
convenience init(color: NSColor, size: NSSize = .init(width: 1, height: 1)) { | ||
self.init(size: size) | ||
lockFocus() | ||
color.drawSwatch(in: NSRect(origin: .zero, size: size)) | ||
unlockFocus() | ||
} | ||
} |
Oops, something went wrong.