Skip to content

Commit

Permalink
Convert App code to Swift
Browse files Browse the repository at this point in the history
  • Loading branch information
dvkch committed Apr 14, 2024
1 parent ff648fd commit fd5a26c
Show file tree
Hide file tree
Showing 38 changed files with 1,109 additions and 1,450 deletions.
213 changes: 111 additions & 102 deletions Online.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions Online/AppDelegate.swift
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?()
}
}
10 changes: 6 additions & 4 deletions Online/Base.lproj/MainMenu.xib
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="8191" systemVersion="15B42" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="8191"/>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
Expand All @@ -11,7 +12,7 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="SYAppDelegate">
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Online" customModuleProvider="target">
<connections>
<outlet property="statusMenu" destination="wsM-49-s7F" id="9UZ-D1-BWa"/>
</connections>
Expand Down Expand Up @@ -665,6 +666,7 @@
</menu>
</menuItem>
</items>
<point key="canvasLocation" x="19" y="123"/>
</menu>
<menu id="wsM-49-s7F">
<point key="canvasLocation" x="896.5" y="176.5"/>
Expand Down
80 changes: 80 additions & 0 deletions Online/Controllers/Crawler.swift
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)
}
}
74 changes: 74 additions & 0 deletions Online/Controllers/Storage.swift
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)
}
}
20 changes: 20 additions & 0 deletions Online/Extensions/NSColor+SY.swift
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
)
}
}
18 changes: 18 additions & 0 deletions Online/Extensions/NSImage+SY.swift
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()
}
}
Loading

0 comments on commit fd5a26c

Please sign in to comment.