🎉 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:
2025-06-28 16:15:15 +02:00
commit 0dabed11d2
63 changed files with 25727 additions and 0 deletions

View File

@@ -0,0 +1,962 @@
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
}
}