SHD Desktop public download instructions

This commit is contained in:
bulava92
2026-10-28 16:59:02 +03:00
commit 4d6e26db88
24 changed files with 2523 additions and 0 deletions
@@ -0,0 +1,702 @@
import Cocoa
import WebKit
import AuthenticationServices
import CryptoKit
import Security
@MainActor
private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
weak var controller: MainWindowController?
init(controller: MainWindowController) {
self.controller = controller
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "shdDesktopAuth" {
let origin = message.frameInfo.securityOrigin
let trustedHosts = ["shd.xyz.su", "dev-shd.xyz.su"]
guard message.frameInfo.isMainFrame,
origin.protocol.lowercased() == "https",
trustedHosts.contains(origin.host.lowercased()) else {
return
}
let environment: MainWindowController.Environment = origin.host.lowercased() == "dev-shd.xyz.su" ? .dev : .production
controller?.signInWithPasskeyInBrowser(for: environment)
return
}
if message.name == "shdNativePrint" {
controller?.printCurrentPage()
}
}
}
@MainActor
final class MainWindowController: NSWindowController, NSWindowDelegate, WKNavigationDelegate, WKUIDelegate, WKDownloadDelegate, ASWebAuthenticationPresentationContextProviding {
enum Environment: String, CaseIterable {
case production = "https://shd.xyz.su"
case dev = "https://dev-shd.xyz.su"
var title: String {
switch self {
case .production: return "SHD Prod"
case .dev: return "SHD Dev"
}
}
}
var webView: WKWebView!
private var progressIndicator: NSProgressIndicator!
private var currentEnv: Environment = .production
private var observation: NSKeyValueObservation?
private var authSession: ASWebAuthenticationSession?
private var authAttemptID: UUID?
private static let authClientID = "shd-desktop"
private static let authCallbackScheme = "shd-auth"
private static let authCallbackURI = "shd-auth://callback"
init(initialEnv: Environment? = nil, initialRequest: URLRequest? = nil) {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1440, height: 920),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.center()
window.title = "SHD"
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.isReleasedWhenClosed = false
window.minSize = NSSize(width: 800, height: 500)
// Dark theme: ensures window and native tabs are always dark matching SHD
window.appearance = NSAppearance(named: .darkAqua)
let darkBg = NSColor(calibratedRed: 0.08, green: 0.08, blue: 0.08, alpha: 1.0)
window.backgroundColor = darkBg
// Native macOS tabs support
window.tabbingMode = .preferred
window.tabbingIdentifier = "SHDMainTabGroup"
super.init(window: window)
window.delegate = self
setupWebView(darkBg: darkBg)
if let initialRequest = initialRequest {
webView.load(initialRequest)
} else if let initialEnv = initialEnv {
currentEnv = initialEnv
navigateToEnvironment(currentEnv)
} else {
loadInitialEnvironment()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupWebView(darkBg: NSColor) {
guard let window = window else { return }
let config = WKWebViewConfiguration()
config.websiteDataStore = WKWebsiteDataStore.default()
config.preferences.isElementFullscreenEnabled = true
config.defaultWebpagePreferences.allowsContentJavaScript = true
// Immediate dark style injection before document parsing even starts
let darkCSS = """
html, body {
background-color: #121212 !important;
color: #e0e0e0 !important;
}
"""
let darkScript = """
(function() {
var el = document.documentElement;
if (el) { el.style.backgroundColor = '#121212'; }
var style = document.createElement('style');
style.id = '__shd_instant_dark__';
style.textContent = '\(darkCSS.replacingOccurrences(of: "\n", with: " "))';
if (document.head) {
document.head.appendChild(style);
} else if (el) {
el.appendChild(style);
}
window.print = function() {
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.shdNativePrint) {
window.webkit.messageHandlers.shdNativePrint.postMessage({});
}
};
})();
"""
let userScript = WKUserScript(source: darkScript, injectionTime: .atDocumentStart, forMainFrameOnly: false)
config.userContentController.addUserScript(userScript)
let passkeyBridgeScript = """
(function() {
document.addEventListener('click', function(event) {
var path = window.location.pathname;
while (path.length > 1 && path.endsWith('/')) {
path = path.slice(0, -1);
}
if (path !== '/login' && path !== '/login/two-factor') {
return;
}
var target = event.target instanceof Element
? event.target
: (event.target && event.target.parentElement);
if (!target || !target.closest('#btnPasskeyLogin, #twoFactorPasskey')) {
return;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
var bridge = window.webkit && window.webkit.messageHandlers
? window.webkit.messageHandlers.shdDesktopAuth
: null;
if (bridge) {
bridge.postMessage('passkey-login');
}
}, true);
})();
"""
config.userContentController.addUserScript(
WKUserScript(source: passkeyBridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: true)
)
config.userContentController.add(WeakScriptMessageHandler(controller: self), name: "shdNativePrint")
config.userContentController.add(WeakScriptMessageHandler(controller: self), name: "shdDesktopAuth")
let container = NSView()
container.translatesAutoresizingMaskIntoConstraints = false
container.wantsLayer = true
container.layer?.backgroundColor = darkBg.cgColor
window.contentView = container
webView = WKWebView(frame: .zero, configuration: config)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.navigationDelegate = self
webView.uiDelegate = self
webView.allowsBackForwardNavigationGestures = true
webView.isInspectable = true
webView.underPageBackgroundColor = darkBg
webView.setValue(false, forKey: "drawsBackground") // Prevents WebKit initial white canvas
container.addSubview(webView)
progressIndicator = NSProgressIndicator()
progressIndicator.isIndeterminate = false
progressIndicator.style = .bar
progressIndicator.minValue = 0.0
progressIndicator.maxValue = 1.0
progressIndicator.translatesAutoresizingMaskIntoConstraints = false
progressIndicator.isDisplayedWhenStopped = false
container.addSubview(progressIndicator)
// Layout: Pin webview to container but respect safe area so tab bar does NOT overlap topbar
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
webView.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor),
webView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
progressIndicator.leadingAnchor.constraint(equalTo: container.leadingAnchor),
progressIndicator.trailingAnchor.constraint(equalTo: container.trailingAnchor),
progressIndicator.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor),
progressIndicator.heightAnchor.constraint(equalToConstant: 2.0)
])
observation = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in
Task { @MainActor [weak self] in
guard let self = self else { return }
self.progressIndicator.doubleValue = webView.estimatedProgress
if webView.estimatedProgress >= 1.0 {
self.progressIndicator.stopAnimation(nil)
self.progressIndicator.isHidden = true
} else {
self.progressIndicator.isHidden = false
self.progressIndicator.startAnimation(nil)
}
self.updateTitle()
}
}
}
private func loadInitialEnvironment() {
let savedEnvRaw = UserDefaults.standard.string(forKey: "SHDSelectedEnvironment")
if let savedEnvRaw = savedEnvRaw, let env = Environment(rawValue: savedEnvRaw) {
currentEnv = env
} else {
currentEnv = .production
}
navigateToEnvironment(currentEnv)
}
func navigateToEnvironment(_ env: Environment) {
currentEnv = env
UserDefaults.standard.set(env.rawValue, forKey: "SHDSelectedEnvironment")
if let url = URL(string: env.rawValue) {
let request = URLRequest(url: url)
webView.load(request)
}
updateTitle()
}
func signInWithPasskeyInBrowser(for environment: Environment) {
guard authSession == nil else { return }
guard environment == .dev else {
presentDesktopAuthError("Вход по passkey в приложении пока подключён только для DEV.")
return
}
do {
let verifier = try Self.randomBase64URLString(byteCount: 32)
let state = try Self.randomBase64URLString(byteCount: 32)
let challenge = Self.base64URLString(for: Data(SHA256.hash(data: Data(verifier.utf8))))
guard let authorizationURL = makeAuthorizationURL(
environment: environment,
state: state,
challenge: challenge
) else {
presentDesktopAuthError("Не удалось подготовить адрес входа.")
return
}
let attemptID = UUID()
authAttemptID = attemptID
let completionHandler: ASWebAuthenticationSession.CompletionHandler = { [weak self] callbackURL, error in
Task { @MainActor [weak self] in
self?.finishBrowserPasskeyLogin(
callbackURL: callbackURL,
error: error,
expectedState: state,
verifier: verifier,
environment: environment,
attemptID: attemptID
)
}
}
let session: ASWebAuthenticationSession
if #available(macOS 14.4, *) {
session = ASWebAuthenticationSession(
url: authorizationURL,
callback: .customScheme(Self.authCallbackScheme),
completionHandler: completionHandler
)
} else {
session = ASWebAuthenticationSession(
url: authorizationURL,
callbackURLScheme: Self.authCallbackScheme,
completionHandler: completionHandler
)
}
session.presentationContextProvider = self
authSession = session
if !session.start() {
authSession = nil
authAttemptID = nil
presentDesktopAuthError("macOS не смог открыть окно входа. Попробуйте ещё раз.")
}
} catch {
presentDesktopAuthError("Не удалось безопасно подготовить вход.")
}
}
private func makeAuthorizationURL(environment: Environment, state: String, challenge: String) -> URL? {
guard var components = URLComponents(string: environment.rawValue) else { return nil }
components.path = "/desktop-auth/authorize"
components.queryItems = [
URLQueryItem(name: "client_id", value: Self.authClientID),
URLQueryItem(name: "redirect_uri", value: Self.authCallbackURI),
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "state", value: state),
URLQueryItem(name: "code_challenge", value: challenge),
URLQueryItem(name: "code_challenge_method", value: "S256")
]
return components.url
}
private func finishBrowserPasskeyLogin(
callbackURL: URL?,
error: Error?,
expectedState: String,
verifier: String,
environment: Environment,
attemptID: UUID
) {
guard authAttemptID == attemptID else { return }
authAttemptID = nil
authSession = nil
if let error {
if (error as? ASWebAuthenticationSessionError)?.code != .canceledLogin {
presentDesktopAuthError("Вход через браузер не завершился. Попробуйте ещё раз.")
}
return
}
guard let callbackURL,
callbackURL.scheme?.lowercased() == Self.authCallbackScheme,
callbackURL.host?.lowercased() == "callback",
let callbackComponents = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let queryItems = callbackComponents.queryItems else {
presentDesktopAuthError("Сервер вернул некорректный ответ входа.")
return
}
if queryItems.contains(where: { $0.name == "error" }) {
presentDesktopAuthError("Сервер не завершил вход. Попробуйте ещё раз.")
return
}
let stateValues = queryItems.filter { $0.name == "state" }
let codeValues = queryItems.filter { $0.name == "code" }
guard stateValues.count == 1,
stateValues[0].value == expectedState,
codeValues.count == 1,
let code = codeValues[0].value,
!code.isEmpty else {
presentDesktopAuthError("Не удалось подтвердить ответ сервера. Начните вход заново.")
return
}
guard var completeComponents = URLComponents(string: environment.rawValue) else {
presentDesktopAuthError("Не удалось завершить вход в выбранном контуре.")
return
}
completeComponents.path = "/desktop-auth/complete"
guard let completeURL = completeComponents.url else {
presentDesktopAuthError("Не удалось завершить вход в выбранном контуре.")
return
}
var request = URLRequest(url: completeURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.setValue("no-store", forHTTPHeaderField: "Cache-Control")
request.httpBody = Self.formEncodedBody([
URLQueryItem(name: "code", value: code),
URLQueryItem(name: "code_verifier", value: verifier)
])
currentEnv = environment
UserDefaults.standard.set(environment.rawValue, forKey: "SHDSelectedEnvironment")
webView.load(request)
}
private func presentDesktopAuthError(_ message: String) {
let alert = NSAlert()
alert.messageText = "Вход в SHD"
alert.informativeText = message
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
window ?? NSApp.keyWindow ?? NSWindow()
}
private static func randomBase64URLString(byteCount: Int) throws -> String {
var bytes = [UInt8](repeating: 0, count: byteCount)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
guard status == errSecSuccess else {
throw DesktopAuthError.randomNumberGenerationFailed
}
return base64URLString(for: Data(bytes))
}
private static func base64URLString(for data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
private static func formEncodedBody(_ items: [URLQueryItem]) -> Data {
var components = URLComponents()
components.queryItems = items
return Data((components.percentEncodedQuery ?? "").utf8)
}
private enum DesktopAuthError: Error {
case randomNumberGenerationFailed
}
func reload() {
webView.reload()
}
func reloadIgnoringCache() {
webView.reloadFromOrigin()
}
func goBack() {
if webView.canGoBack {
webView.goBack()
}
}
func goForward() {
if webView.canGoForward {
webView.goForward()
}
}
func zoomIn() {
webView.pageZoom += 0.1
}
func zoomOut() {
if webView.pageZoom > 0.4 {
webView.pageZoom -= 0.1
}
}
func resetZoom() {
webView.pageZoom = 1.0
}
func showInspector() {
if let inspector = webView.value(forKey: "_inspector") as? NSObject {
inspector.perform(Selector(("show")))
}
}
func showConsole() {
if let inspector = webView.value(forKey: "_inspector") as? NSObject {
inspector.perform(Selector(("showConsole")))
}
}
func printCurrentPage() {
guard let window = window else { return }
let printInfo = NSPrintInfo.shared.copy() as? NSPrintInfo ?? NSPrintInfo.shared
printInfo.horizontalPagination = .fit
printInfo.verticalPagination = .automatic
printInfo.isHorizontallyCentered = true
printInfo.isVerticallyCentered = false
let printOp = webView.printOperation(with: printInfo)
printOp.view?.frame = webView.bounds
printOp.showsPrintPanel = true
printOp.showsProgressPanel = true
printOp.runModal(for: window, delegate: nil, didRun: nil, contextInfo: nil)
}
private func cleanTitle(from raw: String) -> String {
// Strip out repetitive brand suffixes for clean, elegant tab titles
var title = raw
if let range = title.range(of: " — SHD Project") {
title = String(title[..<range.lowerBound])
} else if let range = title.range(of: " - SHD Project") {
title = String(title[..<range.lowerBound])
} else if let range = title.range(of: " | SHD") {
title = String(title[..<range.lowerBound])
}
title = title.trimmingCharacters(in: .whitespacesAndNewlines)
return title.isEmpty ? "SHD" : title
}
private func updateTitle() {
guard let window = window else { return }
let raw = webView.title ?? ""
let clean = cleanTitle(from: raw)
window.title = clean
}
// MARK: - NSWindowDelegate
func windowWillClose(_ notification: Notification) {
authAttemptID = nil
authSession?.cancel()
authSession = nil
webView.configuration.userContentController.removeScriptMessageHandler(forName: "shdNativePrint")
webView.configuration.userContentController.removeScriptMessageHandler(forName: "shdDesktopAuth")
AppDelegate.shared?.removeWindowController(self)
}
// MARK: - WKNavigationDelegate
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
updateTitle()
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences) async -> (WKNavigationActionPolicy, WKWebpagePreferences) {
if navigationAction.shouldPerformDownload {
return (.download, preferences)
}
guard let url = navigationAction.request.url else {
return (.allow, preferences)
}
let host = url.host?.lowercased() ?? ""
let isInternalHost = host == "shd.xyz.su" || host == "dev-shd.xyz.su" || host == "sso.xyz.su" || host == "git.xyz.su" || host.hasSuffix(".xyz.su")
if navigationAction.navigationType == .linkActivated {
let openInNewTab = navigationAction.modifierFlags.contains(.command) || navigationAction.buttonNumber == 2
if isInternalHost {
if openInNewTab {
AppDelegate.shared?.openTab(with: navigationAction.request, relativeTo: self.window)
return (.cancel, preferences)
}
return (.allow, preferences)
} else {
NSWorkspace.shared.open(url)
return (.cancel, preferences)
}
}
if isInternalHost {
return (.allow, preferences)
}
return (.allow, preferences)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
if navigationResponse.canShowMIMEType {
return .allow
}
return .download
}
// MARK: - WKDownloadDelegate
func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
download.delegate = self
}
func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
download.delegate = self
}
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? {
let downloadsDir = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSHomeDirectory())
var destination = downloadsDir.appendingPathComponent(suggestedFilename)
var counter = 1
let baseName = (suggestedFilename as NSString).deletingPathExtension
let ext = (suggestedFilename as NSString).pathExtension
while FileManager.default.fileExists(atPath: destination.path) {
let newName = ext.isEmpty ? "\(baseName) (\(counter))" : "\(baseName) (\(counter)).\(ext)"
destination = downloadsDir.appendingPathComponent(newName)
counter += 1
}
return destination
}
func downloadDidFinish(_ download: WKDownload) {
NSSound(named: "Glass")?.play()
}
// MARK: - WKUIDelegate (Native Alerts, Confirms, Prompts, Media Permissions & File Uploads)
func webView(_ webView: WKWebView, decideMediaCapturePermissionsFor origin: WKSecurityOrigin, initiatedBy frame: WKFrameInfo, type: WKMediaCaptureType) async -> WKPermissionDecision {
let host = origin.host.lowercased()
if host == "shd.xyz.su" || host == "dev-shd.xyz.su" || host.hasSuffix(".xyz.su") {
return .grant
}
return .prompt
}
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if navigationAction.targetFrame == nil {
if let url = navigationAction.request.url {
let host = url.host?.lowercased() ?? ""
if host == "shd.xyz.su" || host == "dev-shd.xyz.su" || host == "sso.xyz.su" || host == "git.xyz.su" || host.hasSuffix(".xyz.su") {
AppDelegate.shared?.openTab(with: navigationAction.request, relativeTo: self.window)
} else {
NSWorkspace.shared.open(url)
}
}
}
return nil
}
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo) async {
let alert = NSAlert()
alert.messageText = "SHD"
alert.informativeText = message
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
if let window = window {
_ = await alert.beginSheetModal(for: window)
} else {
alert.runModal()
}
}
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo) async -> Bool {
let alert = NSAlert()
alert.messageText = "SHD"
alert.informativeText = message
alert.alertStyle = .warning
alert.addButton(withTitle: "Подтвердить")
alert.addButton(withTitle: "Отмена")
let response: NSApplication.ModalResponse
if let window = window {
response = await alert.beginSheetModal(for: window)
} else {
response = alert.runModal()
}
return response == .alertFirstButtonReturn
}
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo) async -> String? {
let alert = NSAlert()
alert.messageText = "SHD"
alert.informativeText = prompt
alert.addButton(withTitle: "OK")
alert.addButton(withTitle: "Отмена")
let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
input.stringValue = defaultText ?? ""
alert.accessoryView = input
let response: NSApplication.ModalResponse
if let window = window {
response = await alert.beginSheetModal(for: window)
} else {
response = alert.runModal()
}
if response == .alertFirstButtonReturn {
return input.stringValue
}
return nil
}
func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo) async -> [URL]? {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = parameters.allowsMultipleSelection
panel.canChooseDirectories = parameters.allowsDirectories
panel.canChooseFiles = true
let response: NSApplication.ModalResponse
if let window = window {
response = await panel.beginSheetModal(for: window)
} else {
response = panel.runModal()
}
if response == .OK {
return panel.urls
}
return nil
}
}
+212
View File
@@ -0,0 +1,212 @@
import Cocoa
import UserNotifications
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
static weak var shared: AppDelegate?
private(set) var windowControllers: [MainWindowController] = []
override init() {
super.init()
AppDelegate.shared = self
}
func applicationDidFinishLaunching(_ notification: Notification) {
setupMainMenu()
setupNotifications()
let initialController = MainWindowController()
addWindowController(initialController)
initialController.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
func addWindowController(_ controller: MainWindowController) {
if !windowControllers.contains(where: { $0 === controller }) {
windowControllers.append(controller)
}
}
func removeWindowController(_ controller: MainWindowController) {
windowControllers.removeAll(where: { $0 === controller })
}
private func setupNotifications() {
let center = UNUserNotificationCenter.current()
center.delegate = self
center.requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in }
}
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .badge, .sound])
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag {
if let first = windowControllers.first {
first.showWindow(nil)
} else {
let controller = MainWindowController()
addWindowController(controller)
controller.showWindow(nil)
}
}
return true
}
private func setupMainMenu() {
let mainMenu = NSMenu()
// App Menu
let appMenuItem = NSMenuItem()
let appMenu = NSMenu(title: "SHD")
appMenu.addItem(withTitle: "О программе SHD", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
appMenu.addItem(NSMenuItem.separator())
appMenu.addItem(withTitle: "Скрыть SHD", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
let hideOthersItem = appMenu.addItem(withTitle: "Скрыть остальные", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h")
hideOthersItem.keyEquivalentModifierMask = [.command, .option]
appMenu.addItem(withTitle: "Показать все", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
appMenu.addItem(NSMenuItem.separator())
appMenu.addItem(withTitle: "Завершить SHD", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
appMenuItem.submenu = appMenu
mainMenu.addItem(appMenuItem)
// File Menu (New Tab, New Window, Print, Close)
let fileMenuItem = NSMenuItem()
let fileMenu = NSMenu(title: "Файл")
fileMenu.addItem(withTitle: "Новая вкладка", action: #selector(onNewTab), keyEquivalent: "t")
fileMenu.addItem(withTitle: "Новое окно", action: #selector(onNewWindow), keyEquivalent: "n")
fileMenu.addItem(NSMenuItem.separator())
fileMenu.addItem(withTitle: "Напечатать…", action: #selector(onPrint), keyEquivalent: "p")
fileMenu.addItem(NSMenuItem.separator())
fileMenu.addItem(withTitle: "Закрыть вкладку", action: #selector(onCloseTabOrWindow), keyEquivalent: "w")
fileMenuItem.submenu = fileMenu
mainMenu.addItem(fileMenuItem)
// Edit Menu
let editMenuItem = NSMenuItem()
let editMenu = NSMenu(title: "Правка")
editMenu.addItem(withTitle: "Отменить", action: Selector(("undo:")), keyEquivalent: "z")
let redoItem = editMenu.addItem(withTitle: "Повторить", action: Selector(("redo:")), keyEquivalent: "z")
redoItem.keyEquivalentModifierMask = [.command, .shift]
editMenu.addItem(NSMenuItem.separator())
editMenu.addItem(withTitle: "Вырезать", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
editMenu.addItem(withTitle: "Скопировать", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
editMenu.addItem(withTitle: "Вставить", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
editMenu.addItem(withTitle: "Выбрать всё", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
editMenuItem.submenu = editMenu
mainMenu.addItem(editMenuItem)
// View Menu
let viewMenuItem = NSMenuItem()
let viewMenu = NSMenu(title: "Вид")
viewMenu.addItem(withTitle: "Обновить", action: #selector(onReload), keyEquivalent: "r")
let hardReload = viewMenu.addItem(withTitle: "Очистить кэш и обновить", action: #selector(onHardReload), keyEquivalent: "r")
hardReload.keyEquivalentModifierMask = [.command, .shift]
viewMenu.addItem(NSMenuItem.separator())
viewMenu.addItem(withTitle: "Увеличить", action: #selector(onZoomIn), keyEquivalent: "+")
viewMenu.addItem(withTitle: "Уменьшить", action: #selector(onZoomOut), keyEquivalent: "-")
viewMenu.addItem(withTitle: "Исходный масштаб", action: #selector(onResetZoom), keyEquivalent: "0")
viewMenu.addItem(NSMenuItem.separator())
viewMenu.addItem(withTitle: "Полноэкранный режим", action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent: "f")
viewMenu.addItem(NSMenuItem.separator())
let inspectorItem = viewMenu.addItem(withTitle: "Инспектор веб-страницы", action: #selector(onShowInspector), keyEquivalent: "i")
inspectorItem.keyEquivalentModifierMask = [.command, .option]
let consoleItem = viewMenu.addItem(withTitle: "Консоль JavaScript", action: #selector(onShowConsole), keyEquivalent: "c")
consoleItem.keyEquivalentModifierMask = [.command, .option]
viewMenuItem.submenu = viewMenu
mainMenu.addItem(viewMenuItem)
// Environment Menu
let envMenuItem = NSMenuItem()
let envMenu = NSMenu(title: "Контур")
let prodItem = envMenu.addItem(withTitle: "SHD Production (shd.xyz.su)", action: #selector(onSelectProd), keyEquivalent: "1")
prodItem.target = self
let devItem = envMenu.addItem(withTitle: "SHD Dev (dev-shd.xyz.su)", action: #selector(onSelectDev), keyEquivalent: "2")
devItem.target = self
envMenuItem.submenu = envMenu
mainMenu.addItem(envMenuItem)
// History Menu
let historyMenuItem = NSMenuItem()
let historyMenu = NSMenu(title: "История")
historyMenu.addItem(withTitle: "Назад", action: #selector(onGoBack), keyEquivalent: "[")
historyMenu.addItem(withTitle: "Вперед", action: #selector(onGoForward), keyEquivalent: "]")
historyMenuItem.submenu = historyMenu
mainMenu.addItem(historyMenuItem)
// Window Menu
let windowMenuItem = NSMenuItem()
let windowMenu = NSMenu(title: "Окно")
windowMenu.addItem(withTitle: "Убрать в Dock", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m")
windowMenu.addItem(withTitle: "Масштабировать", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "")
windowMenu.addItem(NSMenuItem.separator())
windowMenu.addItem(withTitle: "Объединить все окна", action: #selector(NSWindow.mergeAllWindows(_:)), keyEquivalent: "")
windowMenuItem.submenu = windowMenu
mainMenu.addItem(windowMenuItem)
NSApp.mainMenu = mainMenu
NSApp.windowsMenu = windowMenu
}
private var currentActiveController: MainWindowController? {
guard let keyWindow = NSApp.keyWindow else { return windowControllers.first }
return windowControllers.first(where: { $0.window === keyWindow }) ?? windowControllers.first
}
@objc private func onNewWindow() {
let controller = MainWindowController()
addWindowController(controller)
controller.showWindow(nil)
controller.window?.center()
}
@objc private func onNewTab() {
let controller = MainWindowController()
addWindowController(controller)
if let keyWindow = NSApp.keyWindow, let newWindow = controller.window {
keyWindow.addTabbedWindow(newWindow, ordered: .above)
newWindow.makeKeyAndOrderFront(nil)
} else {
controller.showWindow(nil)
}
}
func openTab(with request: URLRequest, relativeTo parentWindow: NSWindow?) {
let controller = MainWindowController(initialRequest: request)
addWindowController(controller)
if let hostWindow = parentWindow ?? NSApp.keyWindow, let newWindow = controller.window {
hostWindow.addTabbedWindow(newWindow, ordered: .above)
newWindow.makeKeyAndOrderFront(nil)
} else {
controller.showWindow(nil)
}
}
@objc private func onCloseTabOrWindow() {
if let keyWindow = NSApp.keyWindow {
keyWindow.performClose(nil)
}
}
@objc private func onPrint() { currentActiveController?.printCurrentPage() }
@objc private func onReload() { currentActiveController?.reload() }
@objc private func onHardReload() { currentActiveController?.reloadIgnoringCache() }
@objc private func onZoomIn() { currentActiveController?.zoomIn() }
@objc private func onZoomOut() { currentActiveController?.zoomOut() }
@objc private func onResetZoom() { currentActiveController?.resetZoom() }
@objc private func onGoBack() { currentActiveController?.goBack() }
@objc private func onGoForward() { currentActiveController?.goForward() }
@objc private func onSelectProd() { currentActiveController?.navigateToEnvironment(.production) }
@objc private func onSelectDev() { currentActiveController?.navigateToEnvironment(.dev) }
@objc private func onShowInspector() { currentActiveController?.showInspector() }
@objc private func onShowConsole() { currentActiveController?.showConsole() }
}
MainActor.assumeIsolated {
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()
}