🎉 ShotScreen v1.0 - Initial Release
🚀 First official release of ShotScreen with complete feature set: ✨ Core Features: - Advanced screenshot capture system - Multi-monitor support - Professional UI/UX design - Automated update system with Sparkle - Apple notarized & code signed 🛠 Technical Excellence: - Native Swift macOS application - Professional build & deployment pipeline - Comprehensive error handling - Memory optimized performance 📦 Distribution Ready: - Professional DMG packaging - Apple notarization complete - No security warnings for users - Ready for public distribution This is the foundation release that establishes ShotScreen as a premium screenshot tool for macOS.
This commit is contained in:
473
ShotScreen/Sources/LicenseEntryView.swift
Normal file
473
ShotScreen/Sources/LicenseEntryView.swift
Normal file
@@ -0,0 +1,473 @@
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
struct LicenseEntryView: View {
|
||||
@ObservedObject private var licenseManager = LicenseManager.shared
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@State private var licenseKey = ""
|
||||
@State private var userName = ""
|
||||
@State private var isVerifying = false
|
||||
@State private var errorMessage = ""
|
||||
@State private var showError = false
|
||||
|
||||
// Add closure for window dismissal
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
init(onDismiss: (() -> Void)? = nil) {
|
||||
self.onDismiss = onDismiss
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// Add minimal top padding since traffic lights are hidden
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
|
||||
VStack(spacing: 24) {
|
||||
// Header Section - similar to SettingsTabView
|
||||
VStack(spacing: 16) {
|
||||
// Replace key icon with ShotScreen icon at native 200x200 resolution
|
||||
Group {
|
||||
if let shotScreenIcon = NSImage(named: "ShotScreenIcon_200x200") {
|
||||
Image(nsImage: shotScreenIcon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 200, height: 200)
|
||||
.cornerRadius(20)
|
||||
} else if let bundle = Bundle.main.url(forResource: "images/ShotScreenIcon_200x200", withExtension: "png"),
|
||||
let nsImage = NSImage(contentsOf: bundle) {
|
||||
Image(nsImage: nsImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 200, height: 200)
|
||||
.cornerRadius(20)
|
||||
} else {
|
||||
// Fallback to system icon if ShotScreenIcon_200x200 not found
|
||||
Image(systemName: "key.fill")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(headerTitle)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(headerSubtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
// Trial Status Section
|
||||
switch licenseManager.licenseStatus {
|
||||
case .trial(let daysLeft):
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Trial Status")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
Divider()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Image(systemName: "clock.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text("\(daysLeft) days remaining in your free trial")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("Already purchased? Enter your license key below to activate the full version.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color.primary.opacity(0.03))
|
||||
.cornerRadius(12)
|
||||
|
||||
case .expired:
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Trial Expired")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.red)
|
||||
Divider()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.red)
|
||||
Text("Your 7-day trial has expired")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("Purchase a license or enter your license key to continue using ShotScreen.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color.red.opacity(0.05))
|
||||
.cornerRadius(12)
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
// License Entry Form Section
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("License Information")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
Divider()
|
||||
}
|
||||
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Your Name")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
TextField("Enter your full name", text: $userName)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.disabled(isVerifying)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("License Key")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
TextField("XXXX-XXXX-XXXX-XXXX", text: $licenseKey)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.disabled(isVerifying)
|
||||
}
|
||||
|
||||
if showError {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.red)
|
||||
Text(errorMessage)
|
||||
.foregroundColor(.red)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color.red.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color.primary.opacity(0.03))
|
||||
.cornerRadius(12)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Bottom Button Section - matching SettingsTabView style
|
||||
HStack {
|
||||
Button(action: activateLicense) {
|
||||
HStack {
|
||||
if isVerifying {
|
||||
ProgressView()
|
||||
.scaleEffect(0.7)
|
||||
}
|
||||
Text(isVerifying ? "Verifying..." : "Activate License")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(!canActivate || isVerifying)
|
||||
.keyboardShortcut(.defaultAction) // Return key = Activate
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Buy License") {
|
||||
openPurchaseURL()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button("Cancel") {
|
||||
dismissWindow()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(minWidth: 600, idealWidth: 650, minHeight: 700, idealHeight: 750)
|
||||
.background(Color.clear)
|
||||
}
|
||||
|
||||
private var canActivate: Bool {
|
||||
!licenseKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
||||
!userName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
private var headerTitle: String {
|
||||
switch licenseManager.licenseStatus {
|
||||
case .trial(_):
|
||||
return "Activate ShotScreen"
|
||||
case .expired:
|
||||
return "Unlock ShotScreen"
|
||||
default:
|
||||
return "Activate ShotScreen"
|
||||
}
|
||||
}
|
||||
|
||||
private var headerSubtitle: String {
|
||||
switch licenseManager.licenseStatus {
|
||||
case .trial(_):
|
||||
return "Already have a license? Enter your license key below."
|
||||
case .expired:
|
||||
return "Your free trial has ended. Enter your license key to continue using ShotScreen."
|
||||
default:
|
||||
return "Enter your license key to unlock the full version"
|
||||
}
|
||||
}
|
||||
|
||||
private func activateLicense() {
|
||||
guard canActivate else { return }
|
||||
|
||||
isVerifying = true
|
||||
showError = false
|
||||
errorMessage = ""
|
||||
|
||||
Task {
|
||||
let success = await licenseManager.enterLicenseKey(
|
||||
licenseKey.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
userName: userName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
isVerifying = false
|
||||
|
||||
if success {
|
||||
dismissWindow()
|
||||
} else {
|
||||
showError = true
|
||||
errorMessage = "Invalid license key or verification failed. Please check your license key and internet connection."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openPurchaseURL() {
|
||||
// ShotScreen Gumroad license product
|
||||
if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissWindow() {
|
||||
onDismiss?()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - License Entry Window
|
||||
class LicenseEntryWindow: NSWindow {
|
||||
private var visualEffectViewContainer: NSView?
|
||||
|
||||
override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {
|
||||
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
|
||||
setupWindow()
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 650, height: 750),
|
||||
styleMask: [.titled, .closable, .resizable, .fullSizeContentView],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
}
|
||||
|
||||
private func setupWindow() {
|
||||
title = "ShotScreen License"
|
||||
isReleasedWhenClosed = false
|
||||
center()
|
||||
|
||||
// Ensure the window appears in front and can be brought to front
|
||||
level = .normal
|
||||
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
|
||||
// Apply same blur style as SettingsWindow
|
||||
isOpaque = false
|
||||
backgroundColor = .clear
|
||||
titlebarAppearsTransparent = true
|
||||
titleVisibility = .hidden
|
||||
isMovableByWindowBackground = true
|
||||
|
||||
// Hide traffic lights (red, yellow, green buttons) to prevent content overlap
|
||||
standardWindowButton(.closeButton)?.isHidden = true
|
||||
standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
standardWindowButton(.zoomButton)?.isHidden = true
|
||||
|
||||
// Create content view with dismissal callback
|
||||
let licenseView = LicenseEntryView(onDismiss: { [weak self] in
|
||||
self?.close()
|
||||
})
|
||||
let hostingView = NSHostingView(rootView: licenseView)
|
||||
|
||||
// Apply same visual effect as SettingsWindow
|
||||
let visualEffectView = NSVisualEffectView()
|
||||
visualEffectView.blendingMode = .behindWindow
|
||||
visualEffectView.material = .hudWindow // Same strong system blur
|
||||
visualEffectView.state = .active
|
||||
visualEffectView.alphaValue = 1.0
|
||||
visualEffectView.autoresizingMask = [.width, .height]
|
||||
|
||||
// Extra blur layer for enhanced effect
|
||||
let extraBlurView = NSVisualEffectView()
|
||||
extraBlurView.blendingMode = .behindWindow
|
||||
extraBlurView.material = .hudWindow
|
||||
extraBlurView.state = .active
|
||||
extraBlurView.alphaValue = 0.6 // Half transparent for optical blur enhancement
|
||||
extraBlurView.autoresizingMask = [.width, .height]
|
||||
|
||||
let newRootContentView = NSView(frame: contentRect(forFrameRect: frame))
|
||||
|
||||
// Layer order: strongest blur at bottom, half blur above, SwiftUI content on top
|
||||
visualEffectView.frame = newRootContentView.bounds
|
||||
extraBlurView.frame = newRootContentView.bounds
|
||||
|
||||
newRootContentView.addSubview(visualEffectView) // Layer 0
|
||||
newRootContentView.addSubview(extraBlurView) // Layer 1 (enhancement)
|
||||
newRootContentView.addSubview(hostingView) // Layer 2 (UI)
|
||||
|
||||
contentView = newRootContentView
|
||||
visualEffectViewContainer = newRootContentView
|
||||
|
||||
hostingView.wantsLayer = true
|
||||
hostingView.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
|
||||
hostingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
hostingView.topAnchor.constraint(equalTo: newRootContentView.topAnchor),
|
||||
hostingView.bottomAnchor.constraint(equalTo: newRootContentView.bottomAnchor),
|
||||
hostingView.leadingAnchor.constraint(equalTo: newRootContentView.leadingAnchor),
|
||||
hostingView.trailingAnchor.constraint(equalTo: newRootContentView.trailingAnchor)
|
||||
])
|
||||
|
||||
// Force a layout pass after setting up the hierarchy
|
||||
contentView?.layoutSubtreeIfNeeded()
|
||||
|
||||
// Window appearance
|
||||
delegate = self
|
||||
|
||||
// Better window management - bring to front
|
||||
orderFrontRegardless()
|
||||
makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
print("🗑️ LicenseEntryWindow deinitialized safely")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Window Delegate
|
||||
extension LicenseEntryWindow: NSWindowDelegate {
|
||||
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
||||
// Allow closing
|
||||
return true
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
// Clean up when window is about to close
|
||||
contentView = nil
|
||||
delegate = nil
|
||||
print("🪟 License window closing gracefully")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Trial Expired View
|
||||
struct TrialExpiredView: View {
|
||||
@ObservedObject private var licenseManager = LicenseManager.shared
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
// Replace with ShotScreen icon using 200x200 source at smaller display size
|
||||
Group {
|
||||
if let shotScreenIcon = NSImage(named: "ShotScreenIcon_200x200") {
|
||||
Image(nsImage: shotScreenIcon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(16)
|
||||
} else if let bundle = Bundle.main.url(forResource: "images/ShotScreenIcon_200x200", withExtension: "png"),
|
||||
let nsImage = NSImage(contentsOf: bundle) {
|
||||
Image(nsImage: nsImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(16)
|
||||
} else {
|
||||
// Fallback icon
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 64))
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
// Title
|
||||
Text("Trial Expired")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
|
||||
// Message
|
||||
Text("Your 7-day trial of ShotScreen has expired.\nPlease purchase a license to continue using the app.")
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
// Buttons - matching new style
|
||||
VStack(spacing: 12) {
|
||||
Button("Purchase License") {
|
||||
openPurchaseURL()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button("Enter License Key") {
|
||||
licenseManager.showLicenseEntryDialog()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button("Quit ShotScreen") {
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.padding(40)
|
||||
.frame(width: 400, height: 350)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
}
|
||||
|
||||
private func openPurchaseURL() {
|
||||
// ShotScreen Gumroad license product
|
||||
if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LicenseEntryView()
|
||||
}
|
||||
Reference in New Issue
Block a user