Files
shotscreen/ShotScreen/Sources/SettingsManager.swift
Nick Roodenrijs 0dabed11d2 🎉 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.
2025-06-28 16:15:15 +02:00

962 lines
42 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AppKit
import SwiftUI // Needed for ObservableObject, Published, etc.
import Combine // Needed for objectWillChange
class SettingsManager: ObservableObject {
static let shared = SettingsManager()
private var isInitializing = false // NIEUWE FLAG
var screenshotFolder: String? {
get { UserDefaults.standard.string(forKey: SettingsKey.screenshotFolder) }
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.screenshotFolder)
}
}
var thumbnailTimer: Int {
get { UserDefaults.standard.integer(forKey: SettingsKey.thumbnailTimer) }
set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.thumbnailTimer) }
}
@Published var closeAfterDrag: Bool = false {
didSet {
UserDefaults.standard.set(closeAfterDrag, forKey: SettingsKey.closeAfterDrag)
if !isInitializing {
NotificationCenter.default.post(name: .closeAfterDragSettingChanged, object: nil)
}
}
}
var showFolderButton: Bool {
get { UserDefaults.standard.bool(forKey: SettingsKey.showFolderButton) }
set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.showFolderButton) }
}
@Published var closeAfterSave: Bool = false {
didSet {
UserDefaults.standard.set(closeAfterSave, forKey: SettingsKey.closeAfterSave)
if !isInitializing {
NotificationCenter.default.post(name: .closeAfterSaveSettingChanged, object: nil)
}
}
}
@Published var playSoundOnCapture: Bool = true {
didSet {
UserDefaults.standard.set(playSoundOnCapture, forKey: SettingsKey.playSoundOnCapture)
if !isInitializing { // CONTROLEER FLAG
NotificationCenter.default.post(name: .playSoundOnCaptureSettingChanged, object: nil)
}
}
}
// NEW: Properties for filename format
var filenamePrefix: String {
get { UserDefaults.standard.string(forKey: SettingsKey.filenamePrefix) ?? "Schermafbeelding" } // Default prefix
set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.filenamePrefix) }
}
var filenameFormatPreset: FilenameFormatPreset {
get {
let rawValue = UserDefaults.standard.integer(forKey: SettingsKey.filenameFormatPreset)
return FilenameFormatPreset(rawValue: rawValue) ?? .macOSStyle // Default naar macOS style
}
set { objectWillChange.send(); UserDefaults.standard.set(newValue.rawValue, forKey: SettingsKey.filenameFormatPreset) }
}
var filenameCustomFormat: String {
get { UserDefaults.standard.string(forKey: SettingsKey.filenameCustomFormat) ?? "{YYYY}-{MM}-{DD}_{hh}.{mm}.{ss}" } // Default custom format
set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.filenameCustomFormat) }
}
// NEW: Properties for action enables
var isRenameActionEnabled: Bool {
get {
let value = UserDefaults.standard.object(forKey: SettingsKey.isRenameActionEnabled) as? Bool ?? true
return value
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.isRenameActionEnabled)
}
}
var isStashActionEnabled: Bool {
get {
let value = UserDefaults.standard.object(forKey: SettingsKey.isStashActionEnabled) as? Bool ?? true
return value
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.isStashActionEnabled)
}
}
var isOCRActionEnabled: Bool {
get {
let value = UserDefaults.standard.object(forKey: SettingsKey.isOCRActionEnabled) as? Bool ?? true
return value
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.isOCRActionEnabled)
}
}
var isClipboardActionEnabled: Bool {
get {
let value = UserDefaults.standard.object(forKey: SettingsKey.isClipboardActionEnabled) as? Bool ?? true
return value
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.isClipboardActionEnabled)
}
}
var isBackgroundRemoveActionEnabled: Bool {
get {
let value = UserDefaults.standard.object(forKey: SettingsKey.isBackgroundRemoveActionEnabled) as? Bool ?? true
return value
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.isBackgroundRemoveActionEnabled)
}
}
var isCancelActionEnabled: Bool {
get {
let value = UserDefaults.standard.object(forKey: SettingsKey.isCancelActionEnabled) as? Bool ?? true
return value
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.isCancelActionEnabled)
}
}
var isRemoveActionEnabled: Bool {
get {
let value = UserDefaults.standard.object(forKey: SettingsKey.isRemoveActionEnabled) as? Bool ?? true
return value
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.isRemoveActionEnabled)
}
}
var isAction3Enabled: Bool {
get {
let value = UserDefaults.standard.object(forKey: SettingsKey.isAction3Enabled) as? Bool ?? false
return value
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.isAction3Enabled)
}
}
var isAction4Enabled: Bool {
get {
let value = UserDefaults.standard.object(forKey: SettingsKey.isAction4Enabled) as? Bool ?? false
return value
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.isAction4Enabled)
}
}
// NEW: Properties for Delete and Cancel Drag actions
var isDeleteActionEnabled: Bool {
get { UserDefaults.standard.object(forKey: SettingsKey.isDeleteActionEnabled) as? Bool ?? true } // Default AAN
set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.isDeleteActionEnabled) }
}
var isCancelDragActionEnabled: Bool {
get { UserDefaults.standard.object(forKey: SettingsKey.isCancelDragActionEnabled) as? Bool ?? true } // Default AAN
set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.isCancelDragActionEnabled) }
}
// 🎨 Background Removal Method Preference
var preferredBackgroundRemovalMethod: BackgroundRemovalMethod {
get {
let rawValue = UserDefaults.standard.string(forKey: SettingsKey.preferredBackgroundRemovalMethod) ?? "auto"
return BackgroundRemovalMethod(rawValue: rawValue) ?? .auto
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue.rawValue, forKey: SettingsKey.preferredBackgroundRemovalMethod)
}
}
// NEW: Property for automatic startup
@Published var startAppOnLogin: Bool = false {
didSet {
UserDefaults.standard.set(startAppOnLogin, forKey: SettingsKey.startAppOnLogin)
if !isInitializing {
NotificationCenter.default.post(name: .startAppOnLoginSettingChanged, object: nil)
}
}
}
// NEW: Property for automatic screenshot saving
var autoSaveScreenshot: Bool {
get { UserDefaults.standard.object(forKey: SettingsKey.autoSaveScreenshot) as? Bool ?? false } // Default false (handmatig beslissen)
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.autoSaveScreenshot)
// Optioneel: post een notificatie als directe actie in ScreenshotApp nodig is
// NotificationCenter.default.post(name: .autoSaveScreenshotChanged, object: nil)
}
}
// VOEG DEZE TOE
static let thumbnailFixedSizeKey = "thumbnailFixedSize"
@Published var thumbnailFixedSize: ThumbnailFixedSize = .medium {
didSet {
UserDefaults.standard.set(thumbnailFixedSize.rawValue, forKey: SettingsManager.thumbnailFixedSizeKey)
if !isInitializing {
NotificationCenter.default.post(name: .thumbnailSizeSettingChanged, object: nil)
}
}
}
// NEW: Property for stash always on top
@Published var stashAlwaysOnTop: Bool = false {
didSet {
UserDefaults.standard.set(stashAlwaysOnTop, forKey: SettingsKey.stashAlwaysOnTop)
if !isInitializing {
NotificationCenter.default.post(name: .stashAlwaysOnTopSettingChanged, object: nil)
}
}
}
// 🔥💎 MEGA NIEUWE STASH PREVIEW SIZE SETTING! 💎🔥
@Published var stashPreviewSize: StashPreviewSize = .medium {
didSet {
UserDefaults.standard.set(stashPreviewSize.rawValue, forKey: SettingsKey.stashPreviewSize)
if !isInitializing {
NotificationCenter.default.post(name: .stashPreviewSizeChanged, object: nil)
}
}
}
// 🔥💥 HYPERMODE STASH GRID CONFIGURATION! 💥🔥
@Published var stashGridMode: StashGridMode = .fixedColumns {
didSet {
UserDefaults.standard.set(stashGridMode.rawValue, forKey: SettingsKey.stashGridMode)
if !isInitializing {
NotificationCenter.default.post(name: .stashGridModeChanged, object: nil)
NotificationCenter.default.post(name: .stashGridConfigChanged, object: nil)
}
}
}
@Published var stashMaxColumns: Int = 2 {
didSet {
// Clamp to 1-5 range for HYPERMODE safety!
let clampedValue = max(1, min(5, stashMaxColumns))
if clampedValue != stashMaxColumns {
stashMaxColumns = clampedValue
return
}
UserDefaults.standard.set(stashMaxColumns, forKey: SettingsKey.stashMaxColumns)
if !isInitializing {
NotificationCenter.default.post(name: .stashGridConfigChanged, object: nil)
}
}
}
@Published var stashMaxRows: Int = 1 {
didSet {
// Clamp to 1-5 range for HYPERMODE safety!
let clampedValue = max(1, min(5, stashMaxRows))
if clampedValue != stashMaxRows {
stashMaxRows = clampedValue
return
}
UserDefaults.standard.set(stashMaxRows, forKey: SettingsKey.stashMaxRows)
if !isInitializing {
NotificationCenter.default.post(name: .stashGridConfigChanged, object: nil)
}
}
}
// 🔥 NIEUW: Persistent stash setting
@Published var persistentStash: Bool = false {
didSet {
UserDefaults.standard.set(persistentStash, forKey: SettingsKey.persistentStash)
if !isInitializing {
NotificationCenter.default.post(name: .persistentStashChanged, object: nil)
}
}
}
// 🔄 UPDATE SETTINGS
@Published var automaticUpdates: Bool = true {
didSet {
UserDefaults.standard.set(automaticUpdates, forKey: SettingsKey.automaticUpdates)
}
}
@Published var includePreReleases: Bool = false {
didSet {
UserDefaults.standard.set(includePreReleases, forKey: SettingsKey.includePreReleases)
}
}
// NEW: Property for hiding desktop icons during screenshots
@Published var hideDesktopIconsDuringScreenshot: Bool = false {
didSet {
UserDefaults.standard.set(hideDesktopIconsDuringScreenshot, forKey: SettingsKey.hideDesktopIconsDuringScreenshot)
if !isInitializing {
NotificationCenter.default.post(name: .hideDesktopIconsSettingChanged, object: nil)
}
print("Setting updated: hideDesktopIconsDuringScreenshot = \(hideDesktopIconsDuringScreenshot)")
}
}
// NEW: Property for hiding desktop widgets during screenshots
@Published var hideDesktopWidgetsDuringScreenshot: Bool = false {
didSet {
UserDefaults.standard.set(hideDesktopWidgetsDuringScreenshot, forKey: SettingsKey.hideDesktopWidgetsDuringScreenshot)
if !isInitializing {
NotificationCenter.default.post(name: .hideDesktopWidgetsSettingChanged, object: nil)
}
print("Setting updated: hideDesktopWidgetsDuringScreenshot = \(hideDesktopWidgetsDuringScreenshot)")
}
}
// 🔊 NEW: Sound volume and type settings
@Published var screenshotSoundVolume: Float = 0.1 {
didSet {
UserDefaults.standard.set(screenshotSoundVolume, forKey: SettingsKey.screenshotSoundVolume)
}
}
@Published var screenshotSoundType: ScreenshotSoundType = .pop {
didSet {
if let data = try? JSONEncoder().encode(screenshotSoundType) {
UserDefaults.standard.set(data, forKey: SettingsKey.screenshotSoundType)
}
}
}
// 🗂 NEW: Cache management settings
@Published var cacheRetentionTime: CacheRetentionTime = .oneWeek {
didSet {
if let data = try? JSONEncoder().encode(cacheRetentionTime) {
UserDefaults.standard.set(data, forKey: SettingsKey.cacheRetentionTime)
}
// 🧪 NIEUW: Send notification for cache retention time changes (except during initialization)
if !isInitializing {
NotificationCenter.default.post(name: NSNotification.Name("cacheRetentionTimeChanged"), object: nil)
}
}
}
// NEW: Property for first launch completion
var hasCompletedFirstLaunch: Bool {
get { UserDefaults.standard.bool(forKey: SettingsKey.hasCompletedFirstLaunch) }
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.hasCompletedFirstLaunch)
}
}
var windowCaptureIncludeCursor: Bool {
get { UserDefaults.standard.bool(forKey: SettingsKey.windowCaptureIncludeCursor) }
set {
objectWillChange.send()
UserDefaults.standard.set(newValue, forKey: SettingsKey.windowCaptureIncludeCursor)
if !isInitializing {
NotificationCenter.default.post(name: .windowCaptureSettingChanged, object: nil)
}
}
}
// NEW: Property for clean desktop screenshots
// VERWIJDERD: cleanDesktopScreenshots property - feature disabled
@Published var saveAfterEdit: Bool = false {
didSet {
UserDefaults.standard.set(saveAfterEdit, forKey: SettingsKey.saveAfterEdit)
NotificationCenter.default.post(name: .saveAfterEditSettingChanged, object: nil)
}
}
var thumbnailDisplayScreen: ThumbnailDisplayScreen {
get {
let rawValue = UserDefaults.standard.string(forKey: SettingsKey.thumbnailDisplayScreen) ?? ThumbnailDisplayScreen.automatic.rawValue
return ThumbnailDisplayScreen(rawValue: rawValue) ?? .automatic
}
set {
objectWillChange.send()
UserDefaults.standard.set(newValue.rawValue, forKey: SettingsKey.thumbnailDisplayScreen)
}
}
static let actionOrderKey = "actionOrder"
@Published var actionOrder: [ActionType] = [] {
didSet {
let orderStrings = actionOrder.map { $0.rawValue }
UserDefaults.standard.set(orderStrings, forKey: SettingsManager.actionOrderKey)
}
}
// NEW: Keyboard shortcut properties
@Published var useCustomShortcut: Bool = false {
didSet {
UserDefaults.standard.set(useCustomShortcut, forKey: SettingsKey.useCustomShortcut)
if !isInitializing {
// Post notification for hotkey change
NotificationCenter.default.post(name: .shortcutSettingChanged, object: nil)
}
}
}
@Published var customShortcutModifiers: UInt = 0 {
didSet {
UserDefaults.standard.set(customShortcutModifiers, forKey: SettingsKey.customShortcutModifiers)
if !isInitializing && useCustomShortcut {
NotificationCenter.default.post(name: .shortcutSettingChanged, object: nil)
}
}
}
@Published var customShortcutKey: UInt16 = 0 {
didSet {
UserDefaults.standard.set(customShortcutKey, forKey: SettingsKey.customShortcutKey)
if !isInitializing && useCustomShortcut {
NotificationCenter.default.post(name: .shortcutSettingChanged, object: nil)
}
}
}
private init() {
isInitializing = true // ZET FLAG
// Laad ALLE properties hier uit UserDefaults
self.screenshotFolder = UserDefaults.standard.string(forKey: SettingsKey.screenshotFolder) // Kan nil zijn
self.filenamePrefix = UserDefaults.standard.string(forKey: SettingsKey.filenamePrefix) ?? "Schermafbeelding"
let presetRaw = UserDefaults.standard.integer(forKey: SettingsKey.filenameFormatPreset)
self.filenameFormatPreset = FilenameFormatPreset(rawValue: presetRaw) ?? .macOSStyle
// Als de opgeslagen rawValue 0 was en .macOSStyle is 0, dan is dit onnodig dubbel.
// Overweeg: if UserDefaults.standard.object(forKey: SettingsKey.filenameFormatPreset) != nil { ... laden ... } else { default }
self.filenameCustomFormat = UserDefaults.standard.string(forKey: SettingsKey.filenameCustomFormat) ?? "{YYYY}-{MM}-{DD}_{hh}.{mm}.{ss}"
self.saveAfterEdit = UserDefaults.standard.bool(forKey: SettingsKey.saveAfterEdit) // Default false als key niet bestaat
self.playSoundOnCapture = UserDefaults.standard.object(forKey: SettingsKey.playSoundOnCapture) as? Bool ?? true
self.thumbnailTimer = UserDefaults.standard.integer(forKey: SettingsKey.thumbnailTimer)
self.closeAfterDrag = UserDefaults.standard.bool(forKey: SettingsKey.closeAfterDrag)
let thumbFixedSizeKeyString = SettingsManager.thumbnailFixedSizeKey
let loadedFixedSizeRawValue = UserDefaults.standard.string(forKey: thumbFixedSizeKeyString)
if let loadedSize = loadedFixedSizeRawValue.flatMap(ThumbnailFixedSize.init) {
if self.thumbnailFixedSize != loadedSize {
self.thumbnailFixedSize = loadedSize
}
} else {
UserDefaults.standard.set(ThumbnailFixedSize.medium.rawValue, forKey: thumbFixedSizeKeyString)
}
self.showFolderButton = UserDefaults.standard.object(forKey: SettingsKey.showFolderButton) as? Bool ?? true
self.startAppOnLogin = UserDefaults.standard.object(forKey: SettingsKey.startAppOnLogin) as? Bool ?? false
self.autoSaveScreenshot = UserDefaults.standard.object(forKey: SettingsKey.autoSaveScreenshot) as? Bool ?? false
self.closeAfterSave = UserDefaults.standard.bool(forKey: SettingsKey.closeAfterSave)
// Stash window border etc. ook hier initialiseren:
// self.stashWindowBorderWidth = UserDefaults.standard.object(forKey: SettingsKey.stashWindowBorderWidth) == nil ? 1.0 : CGFloat(UserDefaults.standard.float(forKey: SettingsKey.stashWindowBorderWidth))
self.isRenameActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isRenameActionEnabled) as? Bool ?? true
self.isStashActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isStashActionEnabled) as? Bool ?? true
self.isOCRActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isOCRActionEnabled) as? Bool ?? true
self.isClipboardActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isClipboardActionEnabled) as? Bool ?? true
self.isBackgroundRemoveActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isBackgroundRemoveActionEnabled) as? Bool ?? true
self.isCancelActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isCancelActionEnabled) as? Bool ?? true
self.isRemoveActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isRemoveActionEnabled) as? Bool ?? true
self.isDeleteActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isDeleteActionEnabled) as? Bool ?? true
self.isCancelDragActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isCancelDragActionEnabled) as? Bool ?? true
self.stashAlwaysOnTop = UserDefaults.standard.object(forKey: SettingsKey.stashAlwaysOnTop) as? Bool ?? false
self.hideDesktopIconsDuringScreenshot = UserDefaults.standard.object(forKey: SettingsKey.hideDesktopIconsDuringScreenshot) as? Bool ?? false
self.hideDesktopWidgetsDuringScreenshot = UserDefaults.standard.object(forKey: SettingsKey.hideDesktopWidgetsDuringScreenshot) as? Bool ?? false
// 🔥💎 MEGA NIEUWE STASH PREVIEW SIZE INIT! 💎🔥
let stashPreviewSizeRaw = UserDefaults.standard.string(forKey: SettingsKey.stashPreviewSize) ?? StashPreviewSize.medium.rawValue
self.stashPreviewSize = StashPreviewSize(rawValue: stashPreviewSizeRaw) ?? .medium
// 🔥💥 HYPERMODE STASH GRID INIT! 💥🔥
let stashGridModeRaw = UserDefaults.standard.string(forKey: SettingsKey.stashGridMode) ?? StashGridMode.fixedColumns.rawValue
self.stashGridMode = StashGridMode(rawValue: stashGridModeRaw) ?? .fixedColumns
self.stashMaxColumns = UserDefaults.standard.object(forKey: SettingsKey.stashMaxColumns) as? Int ?? 2
// HYPERMODE SAFETY: Clamp to 1-5 range!
self.stashMaxColumns = max(1, min(5, self.stashMaxColumns))
self.stashMaxRows = UserDefaults.standard.object(forKey: SettingsKey.stashMaxRows) as? Int ?? 1
// HYPERMODE SAFETY: Clamp to 1-5 range!
self.stashMaxRows = max(1, min(5, self.stashMaxRows))
// 🔥 NIEUW: Persistent stash init
self.persistentStash = UserDefaults.standard.object(forKey: SettingsKey.persistentStash) as? Bool ?? false
// 🔄 UPDATE SETTINGS INIT
self.automaticUpdates = UserDefaults.standard.object(forKey: SettingsKey.automaticUpdates) as? Bool ?? true
self.includePreReleases = UserDefaults.standard.object(forKey: SettingsKey.includePreReleases) as? Bool ?? false
// 🎹 CUSTOM SHORTCUT SETTINGS INIT
self.useCustomShortcut = UserDefaults.standard.object(forKey: SettingsKey.useCustomShortcut) as? Bool ?? false
self.customShortcutModifiers = UserDefaults.standard.object(forKey: SettingsKey.customShortcutModifiers) as? UInt ?? 0
self.customShortcutKey = UserDefaults.standard.object(forKey: SettingsKey.customShortcutKey) as? UInt16 ?? 0
// 🔊 SOUND SETTINGS INIT
self.screenshotSoundVolume = UserDefaults.standard.object(forKey: SettingsKey.screenshotSoundVolume) as? Float ?? 0.1
if let soundTypeData = UserDefaults.standard.data(forKey: SettingsKey.screenshotSoundType),
let soundType = try? JSONDecoder().decode(ScreenshotSoundType.self, from: soundTypeData) {
self.screenshotSoundType = soundType
} else {
self.screenshotSoundType = .pop
}
// 🗂 CACHE MANAGEMENT INIT
if let cacheRetentionData = UserDefaults.standard.data(forKey: SettingsKey.cacheRetentionTime),
let retentionTime = try? JSONDecoder().decode(CacheRetentionTime.self, from: cacheRetentionData) {
self.cacheRetentionTime = retentionTime
} else {
self.cacheRetentionTime = .oneWeek
}
// Initialize thumbnailDisplayScreen
let _ = UserDefaults.standard.string(forKey: SettingsKey.thumbnailDisplayScreen) ?? ThumbnailDisplayScreen.automatic.rawValue
// No need to set it since it's a computed property
// Load action order with migration for new actions
if let savedOrder = UserDefaults.standard.stringArray(forKey: SettingsManager.actionOrderKey) {
var loadedOrder = savedOrder.compactMap { ActionType(rawValue: $0) }
// Migration: ensure all new actions are included
let allActions = ActionType.allCases
for action in allActions {
if !loadedOrder.contains(action) {
loadedOrder.append(action)
}
}
actionOrder = loadedOrder
} else {
actionOrder = ActionType.allCases
}
// Initialize hasCompletedFirstLaunch
self.hasCompletedFirstLaunch = UserDefaults.standard.object(forKey: SettingsKey.hasCompletedFirstLaunch) as? Bool ?? false
isInitializing = false // RESET FLAG
}
private enum CodingKeys: String, CodingKey {
case screenshotFolder
case filenamePrefix
case filenameFormatPreset
case filenameCustomFormat
case saveAfterEdit
case playSoundOnCapture
case thumbnailTimer
case closeAfterDrag
case thumbnailFixedSize
case showFolderButton
case startAppOnLogin
case autoSaveScreenshot
case closeAfterSave
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
screenshotFolder = try container.decodeIfPresent(String.self, forKey: .screenshotFolder)
filenamePrefix = try container.decodeIfPresent(String.self, forKey: .filenamePrefix) ?? "Screenshot"
filenameCustomFormat = try container.decodeIfPresent(String.self, forKey: .filenameCustomFormat) ?? "{YYYY}-{MM}-{DD}_{hh}.{mm}.{ss}"
saveAfterEdit = try container.decodeIfPresent(Bool.self, forKey: .saveAfterEdit) ?? false
playSoundOnCapture = try container.decodeIfPresent(Bool.self, forKey: .playSoundOnCapture) ?? true
thumbnailTimer = try container.decodeIfPresent(Int.self, forKey: .thumbnailTimer) ?? 0
closeAfterDrag = try container.decodeIfPresent(Bool.self, forKey: .closeAfterDrag) ?? false
let presetRaw = try container.decodeIfPresent(Int.self, forKey: .filenameFormatPreset) ?? FilenameFormatPreset.macOSStyle.rawValue
filenameFormatPreset = FilenameFormatPreset(rawValue: presetRaw) ?? .macOSStyle
thumbnailFixedSize = try container.decodeIfPresent(ThumbnailFixedSize.self, forKey: .thumbnailFixedSize) ?? .medium
showFolderButton = try container.decodeIfPresent(Bool.self, forKey: .showFolderButton) ?? true
startAppOnLogin = try container.decodeIfPresent(Bool.self, forKey: .startAppOnLogin) ?? false
autoSaveScreenshot = try container.decodeIfPresent(Bool.self, forKey: .autoSaveScreenshot) ?? false
closeAfterSave = try container.decodeIfPresent(Bool.self, forKey: .closeAfterSave) ?? false
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(screenshotFolder, forKey: .screenshotFolder)
try container.encode(filenamePrefix, forKey: .filenamePrefix)
try container.encode(filenameFormatPreset.rawValue, forKey: .filenameFormatPreset)
try container.encode(filenameCustomFormat, forKey: .filenameCustomFormat)
try container.encode(saveAfterEdit, forKey: .saveAfterEdit)
try container.encode(playSoundOnCapture, forKey: .playSoundOnCapture)
try container.encode(thumbnailTimer, forKey: .thumbnailTimer)
try container.encode(closeAfterDrag, forKey: .closeAfterDrag)
try container.encode(thumbnailFixedSize, forKey: .thumbnailFixedSize)
try container.encode(showFolderButton, forKey: .showFolderButton)
try container.encode(startAppOnLogin, forKey: .startAppOnLogin)
try container.encode(autoSaveScreenshot, forKey: .autoSaveScreenshot)
try container.encode(closeAfterSave, forKey: .closeAfterSave)
}
func resetToDefaults() {
objectWillChange.send() // Stuur eenmalig aan het begin
// Wijs defaults direct toe aan de properties; de didSets zorgen voor opslaan en notificaties.
screenshotFolder = defaultScreenshotFolder()
filenamePrefix = "Schermafbeelding"
filenameFormatPreset = .macOSStyle
filenameCustomFormat = "{YYYY}-{MM}-{DD}_{hh}.{mm}.{ss}"
saveAfterEdit = false
playSoundOnCapture = true
thumbnailTimer = 0
closeAfterDrag = false
thumbnailFixedSize = .medium
showFolderButton = true
startAppOnLogin = false
autoSaveScreenshot = false
closeAfterSave = false
isRenameActionEnabled = true
isStashActionEnabled = true
isOCRActionEnabled = true
isClipboardActionEnabled = true
isCancelActionEnabled = true
isRemoveActionEnabled = true
isDeleteActionEnabled = true
isCancelDragActionEnabled = true
stashAlwaysOnTop = false
hideDesktopIconsDuringScreenshot = false
// 🔥💎 MEGA NIEUWE RESET VOOR STASH PREVIEW SIZE! 💎🔥
stashPreviewSize = .medium
// 🔥💥 HYPERMODE STASH GRID RESET! 💥🔥
stashGridMode = .fixedColumns
stashMaxColumns = 2
stashMaxRows = 1
// 🔥 NIEUW: Reset persistent stash
persistentStash = false
// 🔄 RESET UPDATE SETTINGS
automaticUpdates = true
includePreReleases = false
// 🎹 RESET CUSTOM SHORTCUT SETTINGS
useCustomShortcut = false
customShortcutModifiers = 0
customShortcutKey = 0
// Verwijder de saveSettings() aanroep.
// Verwijder de individuele NotificationCenter.default.post calls hier; didSets handelen dat af.
}
private func defaultScreenshotFolder() -> String? {
// Voorbeeld: probeer ~/Pictures/Screenshots, anders nil
if let picturesURL = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first {
let screenshotsURL = picturesURL.appendingPathComponent("Screenshots")
// Maak de map als hij niet bestaat (optioneel)
// try? FileManager.default.createDirectory(at: screenshotsURL, withIntermediateDirectories: true, attributes: nil)
return screenshotsURL.path
}
return nil
}
func resetActionOrder() {
actionOrder = ActionType.allCases
}
func moveAction(_ action: ActionType, direction: Int) {
guard let currentIndex = actionOrder.firstIndex(of: action) else { return }
let newIndex = currentIndex + direction
guard newIndex >= 0 && newIndex < actionOrder.count else { return }
actionOrder.swapAt(currentIndex, newIndex)
}
// saveSettings() is waarschijnlijk niet nodig als @Published didSets goed werken.
// Als je het toch wilt:
/*
func saveSettings() {
UserDefaults.standard.set(screenshotFolder, forKey: SettingsKey.screenshotFolder)
// ... etc. voor alle settings ...
UserDefaults.standard.synchronize() // Optioneel, gebeurt periodiek
}
*/
}
// MARK: - Settings Window UI (Nieuwe structuur met TabView)
struct SettingsSnapshot {
var screenshotFolder: String?
var thumbnailTimer: Int
var closeAfterDrag: Bool
var thumbnailFixedSize: ThumbnailFixedSize
var showFolderButton: Bool
var closeAfterSave: Bool
var playSoundOnCapture: Bool
var filenamePrefix: String
var filenameFormatPreset: FilenameFormatPreset
var filenameCustomFormat: String
var startAppOnLogin: Bool
var autoSaveScreenshot: Bool
var thumbnailDisplayScreen: ThumbnailDisplayScreen
var stashAlwaysOnTop: Bool
var hideDesktopIconsDuringScreenshot: Bool
static func captureCurrent() -> SettingsSnapshot {
let s = SettingsManager.shared
return SettingsSnapshot(
screenshotFolder: s.screenshotFolder,
thumbnailTimer: s.thumbnailTimer,
closeAfterDrag: s.closeAfterDrag,
thumbnailFixedSize: s.thumbnailFixedSize,
showFolderButton: s.showFolderButton,
closeAfterSave: s.closeAfterSave,
playSoundOnCapture: s.playSoundOnCapture,
filenamePrefix: s.filenamePrefix,
filenameFormatPreset: s.filenameFormatPreset,
filenameCustomFormat: s.filenameCustomFormat,
startAppOnLogin: s.startAppOnLogin,
autoSaveScreenshot: s.autoSaveScreenshot,
thumbnailDisplayScreen: s.thumbnailDisplayScreen,
stashAlwaysOnTop: s.stashAlwaysOnTop,
hideDesktopIconsDuringScreenshot: s.hideDesktopIconsDuringScreenshot
)
}
func apply(to manager: SettingsManager = SettingsManager.shared) {
manager.screenshotFolder = screenshotFolder
manager.thumbnailTimer = thumbnailTimer
manager.closeAfterDrag = closeAfterDrag
manager.thumbnailFixedSize = thumbnailFixedSize
manager.showFolderButton = showFolderButton
manager.closeAfterSave = closeAfterSave
manager.playSoundOnCapture = playSoundOnCapture
manager.filenamePrefix = filenamePrefix
manager.filenameFormatPreset = filenameFormatPreset
manager.filenameCustomFormat = filenameCustomFormat
manager.startAppOnLogin = startAppOnLogin
manager.autoSaveScreenshot = autoSaveScreenshot
manager.thumbnailDisplayScreen = thumbnailDisplayScreen
manager.stashAlwaysOnTop = stashAlwaysOnTop
manager.hideDesktopIconsDuringScreenshot = hideDesktopIconsDuringScreenshot
}
}
// MARK: - Cache Manager
struct CacheManager {
static let shared = CacheManager()
private init() {}
// Get thumbnail directory path
private var thumbnailDirectory: URL {
let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let shotScreenDirectory = appSupportDirectory.appendingPathComponent("ShotScreen")
let thumbnailsDirectory = shotScreenDirectory.appendingPathComponent("Thumbnails")
return thumbnailsDirectory
}
// Calculate cache size in MB
func getCacheSize() -> Double {
do {
let contents = try FileManager.default.contentsOfDirectory(at: thumbnailDirectory, includingPropertiesForKeys: [.fileSizeKey], options: [])
var totalSize: Int64 = 0
for fileURL in contents {
// Skip thumbnail restoration directory from cache size calculation
if fileURL.lastPathComponent == "thumbnail_restoration" {
continue
}
do {
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
if let fileSize = resourceValues.fileSize {
totalSize += Int64(fileSize)
}
} catch {
print("⚠️ Error getting file size for \(fileURL.lastPathComponent): \(error)")
}
}
// Convert bytes to MB
return Double(totalSize) / (1024 * 1024)
} catch {
print("⚠️ Error calculating cache size: \(error)")
return 0.0
}
}
// Get cache file count
func getCacheFileCount() -> Int {
do {
let contents = try FileManager.default.contentsOfDirectory(at: thumbnailDirectory, includingPropertiesForKeys: [.isDirectoryKey], options: [])
var fileCount = 0
for fileURL in contents {
// Skip thumbnail restoration directory
if fileURL.lastPathComponent == "thumbnail_restoration" {
continue
}
// Only count PNG files, not directories
let resourceValues = try? fileURL.resourceValues(forKeys: [.isDirectoryKey])
let isDirectory = resourceValues?.isDirectory ?? false
if !isDirectory && fileURL.pathExtension == "png" {
fileCount += 1
}
}
return fileCount
} catch {
print("⚠️ Error getting cache file count: \(error)")
return 0
}
}
// Clear all cache (except active thumbnails)
func clearCache(preserveActiveThumbnails: Bool = true) -> (deletedFiles: Int, savedSpace: Double) {
do {
let contents = try FileManager.default.contentsOfDirectory(at: thumbnailDirectory, includingPropertiesForKeys: [.fileSizeKey, .creationDateKey], options: [])
var deletedFiles = 0
var savedSpace: Int64 = 0
let activeThumbnailPath = getActiveThumbnailPath()
for fileURL in contents {
// Skip if this is the active thumbnail and we want to preserve it
if preserveActiveThumbnails && fileURL.path == activeThumbnailPath {
print("🔒 Preserving active thumbnail: \(fileURL.lastPathComponent)")
continue
}
// Skip thumbnail restoration directory completely
if fileURL.lastPathComponent == "thumbnail_restoration" {
print("🔒 Preserving thumbnail restoration directory: \(fileURL.lastPathComponent)")
continue
}
// Skip thumbnail restoration backup files
if fileURL.lastPathComponent.contains("latest_backup") {
print("🔒 Preserving thumbnail restoration backup file: \(fileURL.lastPathComponent)")
continue
}
do {
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
let fileSize = resourceValues.fileSize ?? 0
try FileManager.default.removeItem(at: fileURL)
deletedFiles += 1
savedSpace += Int64(fileSize)
print("🗑️ Deleted cache file: \(fileURL.lastPathComponent)")
} catch {
print("⚠️ Failed to delete \(fileURL.lastPathComponent): \(error)")
}
}
let savedSpaceMB = Double(savedSpace) / (1024 * 1024)
print("✅ Cache cleanup complete: \(deletedFiles) files deleted, \(String(format: "%.1f", savedSpaceMB)) MB freed")
return (deletedFiles, savedSpaceMB)
} catch {
print("❌ Error during cache cleanup: \(error)")
return (0, 0.0)
}
}
// Clean cache based on retention time
func cleanupOldCache() {
let retentionTime = SettingsManager.shared.cacheRetentionTime
// Don't cleanup if retention is set to forever
guard let maxAge = retentionTime.timeInterval else {
print("🗂️ Cache retention set to forever - no automatic cleanup")
return
}
do {
let contents = try FileManager.default.contentsOfDirectory(at: thumbnailDirectory, includingPropertiesForKeys: [.creationDateKey, .fileSizeKey], options: [])
let cutoffDate = Date().addingTimeInterval(-maxAge)
let activeThumbnailPath = getActiveThumbnailPath()
print("🧪 CLEANUP: Checking \(contents.count) files. Cutoff time: \(cutoffDate). Max age: \(Int(maxAge))s")
var deletedFiles = 0
var savedSpace: Int64 = 0
var checkedFiles = 0
for fileURL in contents {
// Skip active thumbnail
if fileURL.path == activeThumbnailPath {
print("🧪 CLEANUP: Skipping active thumbnail: \(fileURL.lastPathComponent)")
continue
}
// Skip thumbnail restoration directory completely
if fileURL.lastPathComponent == "thumbnail_restoration" {
continue
}
// Skip thumbnail restoration backup files
if fileURL.lastPathComponent.contains("latest_backup") {
continue
}
do {
let resourceValues = try fileURL.resourceValues(forKeys: [.creationDateKey, .fileSizeKey])
if let creationDate = resourceValues.creationDate {
checkedFiles += 1
let age = Date().timeIntervalSince(creationDate)
print("🧪 CLEANUP: \(fileURL.lastPathComponent) - age: \(Int(age))s, created: \(creationDate)")
if creationDate < cutoffDate {
let fileSize = resourceValues.fileSize ?? 0
try FileManager.default.removeItem(at: fileURL)
deletedFiles += 1
savedSpace += Int64(fileSize)
print("🗑️ DELETED: \(fileURL.lastPathComponent) (was \(Int(age))s old)")
} else {
print("✅ KEEPING: \(fileURL.lastPathComponent) (only \(Int(age))s old)")
}
}
} catch {
print("⚠️ Error processing \(fileURL.lastPathComponent): \(error)")
}
}
if deletedFiles > 0 {
let savedSpaceMB = Double(savedSpace) / (1024 * 1024)
print("✅ Auto cache cleanup: \(deletedFiles) old files deleted, \(String(format: "%.1f", savedSpaceMB)) MB freed")
} else if checkedFiles > 0 {
print("🧪 CLEANUP: No files old enough to delete (\(checkedFiles) files checked)")
} else {
print("🧪 CLEANUP: No cache files found to check")
}
} catch {
print("❌ Error during automatic cache cleanup: \(error)")
}
}
// Get active thumbnail path (to avoid deleting currently open thumbnail)
private func getActiveThumbnailPath() -> String? {
// Try to get the active thumbnail URL from ScreenshotApp
if let app = NSApp.delegate as? ScreenshotApp,
let tempURL = app.getTempURL() {
return tempURL.path
}
return nil
}
}