diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 3094c555..099d6a31 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ 9A81C75E2449A41400825D92 /* RAM.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A81C7562449A41400825D92 /* RAM.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9A81C7692449A43600825D92 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C7672449A43600825D92 /* main.swift */; }; 9A81C76A2449A43600825D92 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C7682449A43600825D92 /* readers.swift */; }; + 9A83526F2889A03100791BAC /* Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A83526E2889A03100791BAC /* Setup.swift */; }; 9A8B923D2696445C00FD6D83 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8B923C2696445C00FD6D83 /* settings.swift */; }; 9A90E19024EAD2BB00471E9A /* GPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A90E18924EAD2BB00471E9A /* GPU.framework */; }; 9A90E19124EAD2BB00471E9A /* GPU.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A90E18924EAD2BB00471E9A /* GPU.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -391,6 +392,7 @@ 9A81C7592449A41400825D92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9A81C7672449A43600825D92 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 9A81C7682449A43600825D92 /* readers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = readers.swift; sourceTree = ""; }; + 9A83526E2889A03100791BAC /* Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setup.swift; sourceTree = ""; }; 9A8B923C2696445C00FD6D83 /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; }; 9A90E18924EAD2BB00471E9A /* GPU.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GPU.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9A90E18C24EAD2BB00471E9A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -733,6 +735,7 @@ 9A045EB62594F8D100ED58F2 /* Dashboard.swift */, 9A81C74B24499C7000825D92 /* AppSettings.swift */, 9A9EA9442476D34500E3B883 /* Update.swift */, + 9A83526E2889A03100791BAC /* Setup.swift */, ); path = Views; sourceTree = ""; @@ -1477,6 +1480,7 @@ 9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */, 9A81C74E24499C7000825D92 /* Settings.swift in Sources */, 9A81C74D24499C7000825D92 /* AppSettings.swift in Sources */, + 9A83526F2889A03100791BAC /* Setup.swift in Sources */, 9AD33AC624BCD3EE007E8820 /* helpers.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index 655e1f09..91411bd5 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -36,6 +36,7 @@ var modules: [Module] = [ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { internal let settingsWindow: SettingsWindow = SettingsWindow() internal let updateWindow: UpdateWindow = UpdateWindow() + internal let setupWindow: SetupWindow = SetupWindow() internal let updateActivity = NSBackgroundActivityScheduler(identifier: "eu.exelban.Stats.updateCheck") internal var clickInNotification: Bool = false @@ -51,9 +52,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele self.parseArguments() self.parseVersion() - - modules.forEach{ $0.mount() } - self.settingsWindow.setModules() + self.setup { + modules.forEach{ $0.mount() } + self.settingsWindow.setModules() + } self.defaultValues() diff --git a/Stats/Views/Setup.swift b/Stats/Views/Setup.swift new file mode 100644 index 00000000..21b76d38 --- /dev/null +++ b/Stats/Views/Setup.swift @@ -0,0 +1,214 @@ +// +// Setup.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 21/07/2022. +// Using Swift 5.0. +// Running on macOS 12.4. +// +// Copyright © 2022 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa +import Kit + +internal protocol setup_p: NSView { + func reset() +} + +private let setupSize: CGSize = CGSize(width: 600, height: 400) + +internal class SetupWindow: NSWindow, NSWindowDelegate { + private let view: SetupContainer = SetupContainer() + private let vc: NSViewController = NSViewController(nibName: nil, bundle: nil) + + public var finishHandler: () -> Void = {} + + init() { + self.vc.view = self.view + + super.init( + contentRect: NSRect( + x: NSScreen.main!.frame.width - self.view.frame.width, + y: NSScreen.main!.frame.height - self.view.frame.height, + width: self.view.frame.width, + height: self.view.frame.height + ), + styleMask: [.closable, .titled], + backing: .buffered, + defer: true + ) + + self.contentViewController = self.vc + self.animationBehavior = .default + self.collectionBehavior = .moveToActiveSpace + self.titlebarAppearsTransparent = true + self.delegate = self + self.title = localizedString("Stats Setup") + + self.center() + self.setIsVisible(false) + + let windowController = NSWindowController() + windowController.window = self + windowController.loadWindow() + } + + public func show() { + self.setIsVisible(true) + } + + public func hide() { + self.close() + self.finishHandler() + } +} + +private class SetupContainer: NSStackView { + private let pages: [setup_p] = [SetupView_1()] + + private var main: NSView = NSView() + private var prevBtn: NSButton = NSButton() + private var nextBtn: NSButton = NSButton() + + init() { + super.init(frame: NSRect(x: 0, y: 0, width: setupSize.width, height: setupSize.height)) + self.orientation = .vertical + self.spacing = 0 + + self.addArrangedSubview(self.main) + self.addArrangedSubview(self.footerView()) + + self.setView(i: 0) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + NSColor.tertiaryLabelColor.set() + let line = NSBezierPath() + line.move(to: NSPoint(x: 0, y: 59)) + line.line(to: NSPoint(x: self.frame.width, y: 59)) + line.lineWidth = 0.25 + line.stroke() + } + + private func footerView() -> NSView { + let container = NSStackView() + container.orientation = .horizontal + + let prev = NSButton() + prev.bezelStyle = .regularSquare + prev.isEnabled = false + prev.title = localizedString("Previous") + prev.toolTip = localizedString("Previous page") + prev.action = #selector(self.prev) + prev.target = self + self.prevBtn = prev + + let next = NSButton() + next.bezelStyle = .regularSquare + next.title = localizedString("Next") + next.toolTip = localizedString("Next page") + next.action = #selector(self.next) + next.target = self + self.nextBtn = next + + container.addArrangedSubview(prev) + container.addArrangedSubview(next) + + NSLayoutConstraint.activate([ + container.heightAnchor.constraint(equalToConstant: 60), + prev.heightAnchor.constraint(equalToConstant: 28), + next.heightAnchor.constraint(equalToConstant: 28) + ]) + + return container + } + + @objc private func prev() { + if let current = self.main.subviews.first, let idx = self.pages.firstIndex(where: { $0 == current }) { + self.setView(i: idx-1) + } + } + + @objc private func next() { + if let current = self.main.subviews.first, let idx = self.pages.firstIndex(where: { $0 == current }) { + if idx+1 >= self.pages.count, let window = self.window as? SetupWindow { + window.hide() + return + } + self.setView(i: idx+1) + } + } + + private func setView(i: Int) { + guard self.pages.indices.contains(i) else { return } + + if i == 0 { + self.prevBtn.isEnabled = false + self.nextBtn.isEnabled = true + } else if i == self.pages.count-1 { + self.nextBtn.title = localizedString("Finish") + self.nextBtn.toolTip = localizedString("Finish setup") + } else { + self.prevBtn.isEnabled = true + self.nextBtn.isEnabled = true + self.nextBtn.title = localizedString("Next") + self.nextBtn.toolTip = localizedString("Next page") + } + + let view = self.pages[i] + view.reset() + + self.main.subviews.forEach({ $0.removeFromSuperview() }) + self.main.addSubview(view) + } +} + +private class SetupView_1: NSStackView, setup_p { + init() { + super.init(frame: NSRect(x: 0, y: 0, width: setupSize.width, height: setupSize.height - 60)) + + let container: NSGridView = NSGridView() + container.heightAnchor.constraint(equalToConstant: 220).isActive = true + container.rowSpacing = 0 + container.yPlacement = .center + container.xPlacement = .center + + let icon: NSImageView = NSImageView(image: NSImage(named: NSImage.Name("AppIcon"))!) + icon.heightAnchor.constraint(equalToConstant: 120).isActive = true + + let name: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: container.frame.width, height: 22)) + name.alignment = .center + name.font = NSFont.systemFont(ofSize: 18, weight: .regular) + name.stringValue = localizedString("Welcome to Stats") + name.toolTip = localizedString("Welcome to Stats") + name.isSelectable = false + + let message: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: container.frame.width, height: 16)) + message.alignment = .center + message.font = NSFont.systemFont(ofSize: 12, weight: .regular) + message.stringValue = localizedString("welcome_message") + message.toolTip = localizedString("welcome_message") + message.isSelectable = false + + container.addRow(with: [icon]) + container.addRow(with: [name]) + container.addRow(with: [message]) + + container.row(at: 1).height = 36 + + self.addArrangedSubview(container) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func reset() {} +} diff --git a/Stats/helpers.swift b/Stats/helpers.swift index 80e317fc..c79528f6 100644 --- a/Stats/helpers.swift +++ b/Stats/helpers.swift @@ -136,6 +136,22 @@ extension AppDelegate { } } + internal func setup(completion: @escaping () -> Void) { + if Store.shared.exist(key: "setupProcess") || Store.shared.exist(key: "runAtLoginInitialized") { + completion() + return + } + + debug("showing the setup window") + + self.setupWindow.show() + self.setupWindow.finishHandler = { + debug("setup is finished, starting the app") + completion() + } + Store.shared.set(key: "setupProcess", value: true) + } + internal func checkForNewVersion(silent: Bool = false) { updater.check { result, error in if error != nil {