├── .gitignore
├── Clipchop.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
└── xcshareddata
│ └── xcschemes
│ ├── Clipchop.xcscheme
│ └── ClipchopTests.xcscheme
├── Clipchop
├── AppDelegate+UNNotifications.swift
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon
│ │ ├── AppIcon-Aerugo.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── icon_128x128.png
│ │ │ ├── icon_128x128@2x@2x.png
│ │ │ ├── icon_16x16.png
│ │ │ ├── icon_16x16@2x@2x.png
│ │ │ ├── icon_256x256.png
│ │ │ ├── icon_256x256@2x@2x.png
│ │ │ ├── icon_32x32.png
│ │ │ ├── icon_32x32@2x@2x.png
│ │ │ ├── icon_512x512.png
│ │ │ └── icon_512x512@2x@2x.png
│ │ ├── AppIcon-Beta.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── icon_128x128.png
│ │ │ ├── icon_128x128@2x@2x.png
│ │ │ ├── icon_16x16.png
│ │ │ ├── icon_16x16@2x@2x.png
│ │ │ ├── icon_256x256.png
│ │ │ ├── icon_256x256@2x@2x.png
│ │ │ ├── icon_32x32.png
│ │ │ ├── icon_32x32@2x@2x.png
│ │ │ ├── icon_512x512.png
│ │ │ └── icon_512x512@2x@2x.png
│ │ ├── AppIcon-HoloGram.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── icon_128x128.png
│ │ │ ├── icon_128x128@2x@2x.png
│ │ │ ├── icon_16x16.png
│ │ │ ├── icon_16x16@2x@2x.png
│ │ │ ├── icon_256x256.png
│ │ │ ├── icon_256x256@2x@2x.png
│ │ │ ├── icon_32x32.png
│ │ │ ├── icon_32x32@2x@2x.png
│ │ │ ├── icon_512x512.png
│ │ │ └── icon_512x512@2x@2x.png
│ │ ├── AppIcon-Stable.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── icon_128x128.png
│ │ │ ├── icon_128x128@2x.png
│ │ │ ├── icon_16x16.png
│ │ │ ├── icon_16x16@2x.png
│ │ │ ├── icon_256x256.png
│ │ │ ├── icon_256x256@2x.png
│ │ │ ├── icon_32x32.png
│ │ │ ├── icon_32x32@2x.png
│ │ │ ├── icon_512x512.png
│ │ │ └── icon_512x512@2x.png
│ │ └── Contents.json
│ ├── Contents.json
│ ├── Empty.imageset
│ │ ├── Contents.json
│ │ └── empty.png
│ └── clipchop.fill.symbolset
│ │ ├── Contents.json
│ │ └── clipchop.fill.svg
├── Assets
│ ├── AppIcon.swift
│ ├── InstalledApp.swift
│ ├── Sound.swift
│ ├── SoundPlayer.swift
│ └── Sounds
│ │ ├── happy-pop.mp3
│ │ ├── marimba-bloop.mp3
│ │ └── tap-notification.mp3
├── Clipboard
│ ├── ClipHistorySearch.swift
│ ├── Clipboard Model
│ │ ├── ClipboardDataProvider.swift
│ │ ├── ClipboardModelEditor.swift
│ │ └── ClipboardModelManager.swift
│ ├── ClipboardController.swift
│ ├── ClipboardManager.swift
│ ├── Data Storage
│ │ ├── ClipboardContent.swift
│ │ ├── ClipboardHistory.swift
│ │ ├── ClipboardModel.xcdatamodeld
│ │ │ └── ClipboardModel.xcdatamodel
│ │ │ │ └── contents
│ │ └── MetadataCache.swift
│ ├── FolderManager.swift
│ └── Formatter.swift
├── Clipchop.entitlements
├── ClipchopApp.swift
├── Extensions
│ ├── AppKit
│ │ ├── NSImage+Extensions.swift
│ │ ├── NSPasteboard.PasteboardType+Extensions.swift
│ │ └── NSWindow+Extensions.swift
│ ├── Color+Extensions.swift
│ ├── Defaults+Extensions.swift
│ ├── Defaults+Structures.swift
│ ├── Foundation
│ │ ├── Bundle+Extensions.swift
│ │ ├── MutableCollection+Extensions.swift
│ │ ├── Notification+Extensions.swift
│ │ └── Set+Extensions.swift
│ ├── KeyboardShortcuts+Extensions.swift
│ ├── SwiftUI
│ │ ├── EnvironmentValues+Extensions.swift
│ │ ├── Transision+Extensions.swift
│ │ ├── View+Extensions.swift
│ │ └── View+Functions.swift
│ └── UNNotification+Extensions.swift
├── Info.plist
├── Localizable.xcstrings
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Utilities
│ ├── DefaultsStack.swift
│ ├── LifecycleHandler.swift
│ ├── Logger.swift
│ ├── NotificationManager.swift
│ ├── ObservationTrackingStream.swift
│ ├── PerimissionsManager.swift
│ ├── Throttler.swift
│ ├── ViewController.swift
│ └── getCursorPosition.swift
└── Views
│ ├── Beginning
│ ├── BeginningView.swift
│ ├── BeginningViewController.swift
│ ├── Pages
│ │ ├── BeginningAllSetPage.swift
│ │ ├── BeginningCustomizationPage.swift
│ │ ├── BeginningHelloPage.swift
│ │ ├── BeginningPermissionsPage.swift
│ │ ├── BeginningShortcutsPage.swift
│ │ └── BeginningTutorialPage.swift
│ └── RoamingButton.swift
│ ├── Clip History
│ ├── ClipHistoryPanel.swift
│ ├── ClipHistoryPanelController.swift
│ ├── ClipHistoryView.swift
│ ├── CollapsedPages.swift
│ ├── EmptyStateView.swift
│ ├── Pages
│ │ ├── CollapsedPages.swift
│ │ ├── EmptyStatePages.swift
│ │ ├── ExpandedPages.swift
│ │ └── TabButton.swift
│ └── Preview Cards
│ │ ├── CardPreviewView 2.swift
│ │ ├── CardPreviewView.swift
│ │ ├── DragManager.swift
│ │ ├── Page
│ │ ├── ColorPreviewPage.swift
│ │ ├── HTMLPreviewPags.swift
│ │ ├── RTFPreviewPage.swift
│ │ └── WebLinkPreviewPage.swift
│ │ └── PreviewContentView.swift
│ ├── Fundamentals
│ ├── FormNavigationLinkLabel.swift
│ ├── ListEmbeddedForm.swift
│ ├── SearchFieldWrapper.swift
│ ├── StaleView.swift
│ ├── VisualEffectView.swift
│ └── WrappingHStack.swift
│ ├── Luminare Settings
│ ├── Custom Luminare Component
│ │ ├── CustomLuminareList.swift
│ │ └── CustomLuminarePicker.swift
│ ├── Luminare Settings Pages
│ │ ├── LuminareAboutSettings.swift
│ │ ├── LuminareCategorizationSettings.swift
│ │ ├── LuminareClipboardSettings.swift
│ │ ├── LuminareCustomizationSettings.swift
│ │ ├── LuminareExcludedAppsSettings.swift
│ │ ├── LuminareFolderSettings.swift
│ │ ├── LuminareGeneralSettings.swift
│ │ └── LuminareKeyboardShortcutsSettings.swift
│ ├── LuminareAcknowledgementsView.swift
│ ├── LuminareManager.swift
│ └── LuminarePreferredColorSchemePicker.swift
│ ├── Menu Bar
│ ├── MenuBarIconView.swift
│ └── MenuBarView.swift
│ └── Settings
│ ├── AcknowledgementsView.swift
│ ├── AppVersionView.swift
│ ├── CopyrightsView.swift
│ ├── NavigationEntry.swift
│ ├── Pages
│ ├── AboutSettingsPage.swift
│ ├── CategorizationPage.swift
│ ├── ClipboardSettingsPage.swift
│ ├── CustomizationSettingsPage.swift
│ ├── ExcludedAppsSettingsPage.swift
│ ├── GeneralSettingsPage.swift
│ ├── SyncingSettingsPage.swift
│ └── TestSettingsPage.swift
│ ├── Sections
│ ├── Categorization
│ │ ├── CategorizationSection.swift
│ │ └── RoundedTagView.swift
│ ├── Clipboard
│ │ ├── ClipboardBehaviorsSection.swift
│ │ └── KeyboardShortcutsSection.swift
│ ├── ColoredPickerRow.swift
│ ├── Customization
│ │ └── AppearanceSection.swift
│ ├── Excluded Apps
│ │ ├── ExcludedAppListSection.swift
│ │ └── InstalledAppsMenu.swift
│ ├── FormSectionListContainer.swift
│ ├── General
│ │ ├── GlobalBehaviorsSection.swift
│ │ └── PermissionsSection.swift
│ └── PreferredColorSchemePicker.swift
│ └── SettingsView.swift
├── ClipchopTests
└── ClipchopTests.swift
├── Docs
├── ADD_A_LOCALIZATION.md
├── Contents
│ └── 简体中文
│ │ ├── Overview.png
│ │ └── Overview2.png
└── 简体中文.md
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS
2 | *.DS_Store
3 |
4 | # Xcode
5 | *.pbxuser
6 | *.mode1v3
7 | *.mode2v3
8 | *.perspectivev3
9 | *.xcuserstate
10 | xcuserdata
11 | Build/
12 |
--------------------------------------------------------------------------------
/Clipchop.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Clipchop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Clipchop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "4729f3c6cd2158907a5b1e656e405b44cd642707b3dd75aad824849595884dec",
3 | "pins" : [
4 | {
5 | "identity" : "defaults",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/sindresorhus/Defaults",
8 | "state" : {
9 | "branch" : "main",
10 | "revision" : "d8a954e69ff13b0f7805f3757c8f8d0c8ef5a8cb"
11 | }
12 | },
13 | {
14 | "identity" : "fulldiskaccess",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/inket/FullDiskAccess",
17 | "state" : {
18 | "revision" : "846e04ea2b84fce843f47d7e7f3421189221829c",
19 | "version" : "1.0.0"
20 | }
21 | },
22 | {
23 | "identity" : "fuse-swift",
24 | "kind" : "remoteSourceControl",
25 | "location" : "https://github.com/krisk/fuse-swift",
26 | "state" : {
27 | "revision" : "26ba868691b2d8b7bf2b1322951eb591be70ccca",
28 | "version" : "1.4.0"
29 | }
30 | },
31 | {
32 | "identity" : "keyboardshortcuts",
33 | "kind" : "remoteSourceControl",
34 | "location" : "https://github.com/sindresorhus/KeyboardShortcuts",
35 | "state" : {
36 | "revision" : "2e5f15581fefb821d4b366e57d817be8bf12aa58",
37 | "version" : "2.0.1"
38 | }
39 | },
40 | {
41 | "identity" : "launchatlogin",
42 | "kind" : "remoteSourceControl",
43 | "location" : "https://github.com/sindresorhus/LaunchAtLogin",
44 | "state" : {
45 | "branch" : "main",
46 | "revision" : "9a894d799269cb591037f9f9cb0961510d4dca81"
47 | }
48 | },
49 | {
50 | "identity" : "menubarextraaccess",
51 | "kind" : "remoteSourceControl",
52 | "location" : "https://github.com/orchetect/MenuBarExtraAccess",
53 | "state" : {
54 | "revision" : "f5896b47e15e114975897354c7e1082c51a2bffd",
55 | "version" : "1.0.5"
56 | }
57 | },
58 | {
59 | "identity" : "sauce",
60 | "kind" : "remoteSourceControl",
61 | "location" : "https://github.com/Clipy/Sauce",
62 | "state" : {
63 | "branch" : "master",
64 | "revision" : "71ff8de6292653a27fa98e901e447e47041e6fad"
65 | }
66 | },
67 | {
68 | "identity" : "settingsaccess",
69 | "kind" : "remoteSourceControl",
70 | "location" : "https://github.com/orchetect/SettingsAccess",
71 | "state" : {
72 | "revision" : "f0b57b4c4c0de66b9f4d6c41d39f1d6ede0c9891",
73 | "version" : "2.0.0"
74 | }
75 | },
76 | {
77 | "identity" : "sfsafesymbols",
78 | "kind" : "remoteSourceControl",
79 | "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
80 | "state" : {
81 | "revision" : "e2e28f4e56e1769c2ec3c61c9355fc64eb7a535a",
82 | "version" : "5.3.0"
83 | }
84 | },
85 | {
86 | "identity" : "swift-algorithms",
87 | "kind" : "remoteSourceControl",
88 | "location" : "https://github.com/apple/swift-algorithms.git",
89 | "state" : {
90 | "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42",
91 | "version" : "1.2.0"
92 | }
93 | },
94 | {
95 | "identity" : "swift-numerics",
96 | "kind" : "remoteSourceControl",
97 | "location" : "https://github.com/apple/swift-numerics.git",
98 | "state" : {
99 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b",
100 | "version" : "1.0.2"
101 | }
102 | },
103 | {
104 | "identity" : "swifthexcolors",
105 | "kind" : "remoteSourceControl",
106 | "location" : "https://github.com/thii/SwiftHEXColors",
107 | "state" : {
108 | "revision" : "1f886cb20fedda14a5ac75efd09bc99c95b43a18",
109 | "version" : "1.4.1"
110 | }
111 | },
112 | {
113 | "identity" : "swiftui-introspect",
114 | "kind" : "remoteSourceControl",
115 | "location" : "https://github.com/siteline/swiftui-introspect",
116 | "state" : {
117 | "revision" : "668a65735751432b640260c56dfa621cec568368",
118 | "version" : "1.2.0"
119 | }
120 | },
121 | {
122 | "identity" : "swiftui-variadic-views",
123 | "kind" : "remoteSourceControl",
124 | "location" : "https://github.com/lorenzofiamingo/swiftui-variadic-views",
125 | "state" : {
126 | "revision" : "af67d0e85bd2b499fbfb4cda834117e7c52b12b0",
127 | "version" : "1.0.0"
128 | }
129 | },
130 | {
131 | "identity" : "swiftui-windowmanagement",
132 | "kind" : "remoteSourceControl",
133 | "location" : "https://github.com/Wouter01/SwiftUI-WindowManagement",
134 | "state" : {
135 | "revision" : "adbebf5d7df325f3d7bf07dc832e5e162a9003f5",
136 | "version" : "2.1.1"
137 | }
138 | }
139 | ],
140 | "version" : 3
141 | }
142 |
--------------------------------------------------------------------------------
/Clipchop.xcodeproj/xcshareddata/xcschemes/Clipchop.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
36 |
42 |
43 |
44 |
45 |
46 |
56 |
58 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Clipchop.xcodeproj/xcshareddata/xcschemes/ClipchopTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
17 |
20 |
26 |
27 |
28 |
29 |
30 |
40 |
41 |
47 |
48 |
50 |
51 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/Clipchop/AppDelegate+UNNotifications.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/8/13.
6 | //
7 |
8 | import SwiftUI
9 | import UserNotifications
10 |
11 | extension AppDelegate: UNUserNotificationCenterDelegate {
12 | // Implementation is necessary to show notifications even when the app has focus!
13 | func userNotificationCenter(
14 | _: UNUserNotificationCenter,
15 | willPresent _: UNNotification,
16 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> ()
17 | ) {
18 | completionHandler([.banner])
19 | }
20 |
21 | static func requestNotificationAuthorization() {
22 | UNUserNotificationCenter.current().requestAuthorization(
23 | options: [.alert]
24 | ) { accepted, error in
25 | if !accepted {
26 | print("User Notification access denied.")
27 | }
28 | if let error {
29 | print(error.localizedDescription)
30 | }
31 | }
32 | }
33 |
34 | static func areNotificationsEnabled() -> Bool {
35 | let group = DispatchGroup()
36 | group.enter()
37 |
38 | var notificationsEnabled = false
39 |
40 | UNUserNotificationCenter.current().getNotificationSettings { notificationSettings in
41 | notificationsEnabled = notificationSettings.authorizationStatus != UNAuthorizationStatus.denied
42 | group.leave()
43 | }
44 |
45 | group.wait()
46 | return notificationsEnabled
47 | }
48 |
49 | static func sendNotification(_ content: UNMutableNotificationContent) {
50 | let uuidString = UUID().uuidString
51 | let request = UNNotificationRequest(
52 | identifier: uuidString,
53 | content: content,
54 | trigger: nil
55 | )
56 |
57 | requestNotificationAuthorization()
58 |
59 | UNUserNotificationCenter.current().add(request)
60 | }
61 |
62 | static func sendNotification(_ title: String, _ body: String) {
63 | let content = UNMutableNotificationContent()
64 |
65 | content.title = title
66 | content.body = body
67 | content.categoryIdentifier = UUID().uuidString
68 |
69 | AppDelegate.sendNotification(content)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Clipchop/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/5.
6 | //
7 |
8 | import AppKit
9 | import Defaults
10 | import UserNotifications
11 |
12 | class AppDelegate: NSObject, NSApplicationDelegate {
13 |
14 | static var isActive: Bool = false
15 |
16 | func applicationDidFinishLaunching(_ notification: Notification) {
17 | NSApp.setActivationPolicy(.accessory)
18 | UNUserNotificationCenter.current().delegate = self
19 | AppDelegate.requestNotificationAuthorization()
20 | applyColorScheme()
21 | }
22 |
23 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
24 | NSApp.setActivationPolicy(.accessory)
25 | LuminareManager.fullyClose()
26 | return false
27 | }
28 |
29 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
30 | LuminareManager.open()
31 | return true
32 | }
33 |
34 | private func applyColorScheme() {
35 | switch Defaults[.preferredColorScheme] {
36 | case .system:
37 | NSApplication.shared.appearance = nil
38 | case .light:
39 | NSApplication.shared.appearance = NSAppearance(named: .aqua)
40 | case .dark:
41 | NSApplication.shared.appearance = NSAppearance(named: .darkAqua)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.596",
9 | "green" : "0.675",
10 | "red" : "0.043"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon_16x16@2x@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon_32x32@2x@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon_128x128@2x@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon_256x256@2x@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon_512x512@2x@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_128x128@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_128x128@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_16x16@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_16x16@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_256x256@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_256x256@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_32x32@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_32x32@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_512x512@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Aerugo.appiconset/icon_512x512@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon_16x16@2x@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon_32x32@2x@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon_128x128@2x@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon_256x256@2x@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon_512x512@2x@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_128x128@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_128x128@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_16x16@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_16x16@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_256x256@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_256x256@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_32x32@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_32x32@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_512x512@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Beta.appiconset/icon_512x512@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon_16x16@2x@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon_32x32@2x@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon_128x128@2x@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon_256x256@2x@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon_512x512@2x@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_128x128@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_128x128@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_16x16@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_16x16@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_256x256@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_256x256@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_32x32@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_32x32@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_512x512@2x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-HoloGram.appiconset/icon_512x512@2x@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon_16x16@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon_32x32@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon_128x128@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon_256x256@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon_512x512@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/AppIcon/AppIcon-Stable.appiconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/AppIcon/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/Empty.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "empty.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/Empty.imageset/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets.xcassets/Empty.imageset/empty.png
--------------------------------------------------------------------------------
/Clipchop/Assets.xcassets/clipchop.fill.symbolset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "symbols" : [
7 | {
8 | "filename" : "clipchop.fill.svg",
9 | "idiom" : "universal"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/Clipchop/Assets/AppIcon.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppIcon.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/12.
6 | //
7 |
8 | import SwiftUI
9 | import Defaults
10 | import UserNotifications
11 | import Intents
12 |
13 | struct AppIcon: Hashable, Defaults.Serializable {
14 | var name: String?
15 | var assetName: String
16 | var unlockThreshold: Int
17 | var unlockMessage: String?
18 |
19 | var image: NSImage {
20 | .init(named: assetName)!
21 | }
22 |
23 | func setAppIcon() {
24 | Self.setAppIcon(to: self)
25 | }
26 |
27 | struct Bridge: Defaults.Bridge {
28 | typealias Value = AppIcon
29 | typealias Serializable = String
30 |
31 | func serialize(_ value: AppIcon?) -> String? {
32 | value?.assetName
33 | }
34 |
35 | func deserialize(_ object: String?) -> AppIcon? {
36 | if let object {
37 | return .icons.first { $0.assetName == object }
38 | } else {
39 | return .defaultAppIcon
40 | }
41 | }
42 | }
43 |
44 | static let bridge = Bridge()
45 | }
46 |
47 | extension AppIcon {
48 | static let stable = AppIcon(
49 | name: .init(localized: "App Icon: Stable", defaultValue: "Clipchop"),
50 | assetName: "AppIcon-Stable",
51 | unlockThreshold: 0
52 | )
53 |
54 | static let beta = AppIcon(
55 | name: .init(localized: "App Icon: Beta", defaultValue: "Clipchop Beta"),
56 | assetName: "AppIcon-Beta",
57 | unlockThreshold: 0
58 | )
59 |
60 | static let aerugo = AppIcon(
61 | name: .init(localized: "App Icon: Aerugo", defaultValue: "Aerugo"),
62 | assetName: "AppIcon-Aerugo",
63 | unlockThreshold: 25,
64 | unlockMessage: .init(localized: "Aerugo", defaultValue: "\(Bundle.main.appName) will rust if you don't clip soon.")
65 | )
66 |
67 | static let holoGram = AppIcon(
68 | name: .init(localized: "App Icon: HoloGram", defaultValue: "HoloGram"),
69 | assetName: "AppIcon-HoloGram",
70 | unlockThreshold: 50,
71 | unlockMessage: .init(localized: "HoloGram", defaultValue: "\(Bundle.main.appName) feels a lot stronger!")
72 | )
73 | }
74 |
75 | extension AppIcon {
76 | static var defaultAppIcon: AppIcon {
77 | #if DEBUG
78 | return beta
79 | #else
80 | return stable
81 | #endif
82 | }
83 |
84 | static var currentAppIcon: AppIcon {
85 | Defaults[.appIcon]
86 | }
87 |
88 | static let icons: [AppIcon] = [
89 | stable,
90 | beta,
91 | aerugo,
92 | holoGram
93 | ]
94 | }
95 |
96 | extension AppIcon {
97 | static var unlockedAppIcons: [AppIcon] {
98 | var returnValue: [AppIcon] = []
99 | for icon in icons where icon.unlockThreshold <= Defaults[.timesClipped] {
100 | returnValue.append(icon)
101 | }
102 | return returnValue.reversed()
103 | }
104 |
105 | static func setAppIcon(to icon: AppIcon) {
106 | log(self, "App icon set to: \(icon.assetName)")
107 | Defaults[.appIcon] = icon
108 | refreshCurrentAppIcon()
109 | }
110 |
111 | static func refreshCurrentAppIcon() {
112 | let image = Defaults[.appIcon].image
113 |
114 | NSWorkspace.shared.setIcon(
115 | image,
116 | forFile: Bundle.main.bundlePath,
117 | options: []
118 | )
119 |
120 | NSApp.applicationIconImage = image
121 | }
122 | }
123 |
124 | extension AppIcon {
125 | static func checkIfUnlockedNewIcon() {
126 | guard Defaults[.sendNotification] else { return }
127 |
128 | for icon in icons where icon.unlockThreshold == Defaults[.timesClipped] {
129 | let title = Bundle.main.appName
130 | let body = icon.unlockMessage ?? "You've unlocked a new icon: \(icon.name ?? "Unknown")!"
131 |
132 | let content = UNMutableNotificationContent()
133 | content.title = title
134 | content.body = body
135 |
136 | if let imageData = NSImage(named: icon.assetName)?.tiffRepresentation,
137 | let attachment = UNNotificationAttachment.create(NSData(data: imageData)) {
138 | content.attachments = [attachment]
139 | content.userInfo = ["icon": icon.assetName]
140 | }
141 |
142 | content.categoryIdentifier = "icon_unlocked"
143 |
144 | AppDelegate.sendNotification(content)
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Clipchop/Assets/InstalledApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InstalledApp.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/12.
6 | //
7 |
8 | import SwiftUI
9 | import Algorithms
10 | import Defaults
11 |
12 |
13 | struct InstalledApp: Identifiable {
14 | var id: String { bundleID }
15 | var bundleID: String
16 | var icon: NSImage
17 | var displayName: String
18 | var installationFolder: String
19 | }
20 |
21 | class InstalledApps: ObservableObject {
22 | private var systemQuery = NSMetadataQuery()
23 | private var localQuery = NSMetadataQuery()
24 |
25 | @Published var systemApps = [InstalledApp]()
26 | @Published var installedApps = [InstalledApp]()
27 |
28 | init() {
29 | self.ensureFinderApp()
30 |
31 | self.startQuery(&systemQuery, in: .systemDomainMask) {
32 | self.queryDidFinishGathering(self.systemQuery, to: &self.systemApps, notification: $0)
33 | self.ensureFinderApp()
34 | }
35 |
36 | self.startQuery(&localQuery, in: .localDomainMask) {
37 | self.queryDidFinishGathering(self.localQuery, to: &self.installedApps, notification: $0)
38 | }
39 | }
40 |
41 | deinit {
42 | systemQuery.stop()
43 | localQuery.stop()
44 | }
45 |
46 | private func startQuery(
47 | _ query: inout NSMetadataQuery,
48 | `in`: FileManager.SearchPathDomainMask,
49 | completion: @escaping @Sendable (Notification) -> ()
50 | ) {
51 | query.predicate = NSPredicate(format: "kMDItemContentType == 'com.apple.application-bundle'")
52 | if let appFolder = FileManager.default.urls(
53 | for: .allApplicationsDirectory,
54 | in: `in`
55 | ).first {
56 | query.searchScopes = [appFolder]
57 | }
58 |
59 | NotificationCenter.default.addObserver(
60 | forName: .NSMetadataQueryDidFinishGathering,
61 | object: nil,
62 | queue: nil,
63 | using: completion
64 | )
65 |
66 | query.start()
67 | }
68 |
69 | private func queryDidFinishGathering(
70 | _ query: NSMetadataQuery, to: inout [InstalledApp],
71 | notification: Notification
72 | ) {
73 | if let items = query.results as? [NSMetadataItem] {
74 | to = items.compactMap { item in
75 | guard
76 | let bundleId = item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String,
77 | let displayName = item.value(forAttribute: NSMetadataItemDisplayNameKey) as? String,
78 | let path = item.value(forAttribute: NSMetadataItemPathKey) as? String,
79 | let installationFolder = URL(string: path)?.deletingLastPathComponent().absoluteString.removingPercentEncoding
80 | else {
81 | return nil
82 | }
83 |
84 | let icon = NSWorkspace.shared.icon(forFile: path)
85 | return .init(
86 | bundleID: bundleId,
87 | icon: icon,
88 | displayName: displayName,
89 | installationFolder: installationFolder
90 | )
91 | }
92 | }
93 | }
94 |
95 | private func ensureFinderApp() {
96 | let finderBundleID = "com.apple.finder"
97 |
98 | if !systemApps.contains(where: { $0.bundleID == finderBundleID }) {
99 | let finderPath = "/System/Library/CoreServices/Finder.app"
100 | let icon = NSWorkspace.shared.icon(forFile: finderPath)
101 | let displayName = FileManager.default.displayName(atPath: finderPath)
102 | let installationFolder = (finderPath as NSString).deletingLastPathComponent
103 |
104 | let finderApp = InstalledApp(
105 | bundleID: finderBundleID,
106 | icon: icon,
107 | displayName: displayName,
108 | installationFolder: installationFolder
109 | )
110 | self.systemApps.append(finderApp)
111 | }
112 | }
113 |
114 | func displayName(for bundleID: String) -> String? {
115 | return (systemApps + installedApps).first { $0.bundleID == bundleID }?.displayName
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Clipchop/Assets/Sound.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sound.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/12.
6 | //
7 |
8 | import SwiftUI
9 | import Defaults
10 |
11 | struct Sound: Hashable, Defaults.Serializable {
12 | var hasSound: Bool = true
13 | var name: String?
14 | var assetName: String
15 | var unlockThreshold: Int
16 |
17 | func play() {
18 | Self.play(sound: self)
19 | }
20 |
21 | func setClipSound() {
22 | print("setClipSound")
23 | Self.setClipSound(to: self)
24 | }
25 |
26 | func setPasteSound() {
27 | print("setPasteSound")
28 | Self.setPasteSound(to: self)
29 | }
30 |
31 | struct Bridge: Defaults.Bridge {
32 | typealias Value = Sound
33 | typealias Serializable = String
34 |
35 | func serialize(_ value: Sound?) -> String? {
36 | value?.assetName
37 | }
38 |
39 | func deserialize(_ object: String?) -> Sound? {
40 | if let object {
41 | return .sounds.first { $0.assetName == object }
42 | } else {
43 | return .pop
44 | }
45 | }
46 | }
47 |
48 | static let bridge = Bridge()
49 | }
50 |
51 | extension Sound {
52 | static let none = Sound(
53 | hasSound: false,
54 | name: .init(localized: "Sound: None", defaultValue: "None"),
55 | assetName: "",
56 | unlockThreshold: 0
57 | )
58 |
59 | static let pop = Sound(
60 | name: .init(localized: "Sound: Pop", defaultValue: "Pop"),
61 | assetName: "happy-pop",
62 | unlockThreshold: 0
63 | )
64 |
65 | static let bloop = Sound(
66 | name: .init(localized: "Sound: Bloop", defaultValue: "Bloop"),
67 | assetName: "marimba-bloop",
68 | unlockThreshold: 0
69 | )
70 |
71 | static let tap = Sound(
72 | name: .init(localized: "Sound: Tap", defaultValue: "Tap"),
73 | assetName: "tap-notification",
74 | unlockThreshold: 25
75 | )
76 | }
77 |
78 | extension Sound {
79 | static var defaultClipSound: Sound {
80 | pop
81 | }
82 | static var defaultPasteSound: Sound {
83 | bloop
84 | }
85 |
86 | static var clipSound: Sound {
87 | Defaults[.clipSound]
88 | }
89 | static var pasteSound: Sound {
90 | Defaults[.pasteSound]
91 | }
92 |
93 | static let sounds: [Sound] = [
94 | none,
95 | pop,
96 | bloop,
97 | tap
98 | ]
99 | }
100 |
101 | extension Sound {
102 | static var unlockedSounds: [Sound] {
103 | var returnValue: [Sound] = []
104 | for sound in sounds where sound.unlockThreshold <= Defaults[.timesClipped] {
105 | returnValue.append(sound)
106 | }
107 | return returnValue.reversed()
108 | }
109 |
110 | static func play(sound: Sound) {
111 | if sound.hasSound {
112 | SoundPlayer.playSound(named: sound.assetName, volume: Defaults[.volume])
113 | }
114 | }
115 |
116 | static func setClipSound(to sound: Sound) {
117 | log(self, "Clip sound set to: \(sound.assetName)")
118 | Defaults[.clipSound] = sound
119 | }
120 |
121 | static func setPasteSound(to sound: Sound) {
122 | log(self, "Paste sound set to: \(sound.assetName)")
123 | Defaults[.pasteSound] = sound
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Clipchop/Assets/SoundPlayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoundPlayer.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/12.
6 | //
7 |
8 | import AVFAudio
9 |
10 | class SoundPlayer: Identifiable {
11 | private static var audioPlayer: AVAudioPlayer?
12 | static func playSound(named assetName: String, volume: Float = 1.0) {
13 | if let soundURL = Bundle.main.url(forResource: assetName, withExtension: "mp3") {
14 | do {
15 | audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
16 | audioPlayer?.volume = volume
17 | audioPlayer?.play()
18 |
19 | log(self, "Audio played: \(assetName) at volume: \(volume)")
20 | } catch {
21 | log(self, "Unable to play audio: \(error.localizedDescription)")
22 | }
23 | } else {
24 | log(self, "Audio file not found for \(assetName)!")
25 | }
26 | }
27 |
28 | static func setVolume(_ volume: Float) {
29 | guard let player = audioPlayer else {
30 | log(self, "Audio player not initialized.")
31 | return
32 | }
33 | player.volume = volume
34 | log(self, "Volume set to \(volume)")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Clipchop/Assets/Sounds/happy-pop.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets/Sounds/happy-pop.mp3
--------------------------------------------------------------------------------
/Clipchop/Assets/Sounds/marimba-bloop.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets/Sounds/marimba-bloop.mp3
--------------------------------------------------------------------------------
/Clipchop/Assets/Sounds/tap-notification.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cement-Labs/Clipchop/c6010a8ba9421c3508f1ac7edb05aca33f81b5c6/Clipchop/Assets/Sounds/tap-notification.mp3
--------------------------------------------------------------------------------
/Clipchop/Clipboard/Clipboard Model/ClipboardDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClipboardDataProvider.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/5.
6 | //
7 |
8 | import CoreData
9 |
10 | final class ClipboardDataProvider {
11 |
12 | static let shared = ClipboardDataProvider()
13 |
14 | private let persistentContainer : NSPersistentContainer
15 |
16 | var viewContext: NSManagedObjectContext {
17 | persistentContainer.viewContext
18 | }
19 |
20 | var newContext: NSManagedObjectContext {
21 | persistentContainer.newBackgroundContext()
22 | }
23 |
24 | private init() {
25 | persistentContainer = NSPersistentContainer(name: "ClipboardModel")
26 |
27 | persistentContainer.viewContext.undoManager = UndoManager()
28 |
29 | persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
30 |
31 | persistentContainer.loadPersistentStores { _, error in
32 | if let error {
33 | fatalError("Unale to load data! \(error)")
34 | }
35 | }
36 | }
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/Clipchop/Clipboard/Clipboard Model/ClipboardModelEditor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClipboardModelEditor.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/5.
6 | //
7 |
8 | import CoreData
9 |
10 | final class ClipboardModelEditor {
11 |
12 | @Published var item: ClipboardHistory
13 |
14 | static let shared = ClipboardModelEditor(provider: .shared)
15 | private let context: NSManagedObjectContext
16 |
17 | init(provider: ClipboardDataProvider, item: ClipboardHistory? = nil) {
18 | self.context = provider.newContext
19 | if let item {
20 | self.item = item
21 | } else {
22 | self.item = ClipboardHistory(context: self.context)
23 | }
24 | }
25 |
26 | func deleteAll(ignoresPinned: Bool = true) throws {
27 | let historyFetchRequest = ClipboardHistory.fetchRequest()
28 | let contentFetchRequest = ClipboardContent.fetchRequest()
29 | do {
30 | let histories = try context.fetch(historyFetchRequest)
31 | histories.forEach(context.delete(_:))
32 |
33 | let contents = try context.fetch(contentFetchRequest)
34 | contents.forEach(context.delete(_:))
35 |
36 | try context.save()
37 | context.refreshAllObjects()
38 | log(self,"Deleted all clipboard data")
39 | } catch {
40 | let nsError = error as NSError
41 | log(self,"Error deleting all clipboard data! \(nsError), \(nsError.userInfo)")
42 |
43 | throw nsError
44 | }
45 | }
46 |
47 | func deleteAllExceptPinned() throws {
48 | let fetchRequest = NSFetchRequest(entityName: "ClipboardHistory")
49 | fetchRequest.predicate = NSPredicate(format: "pin == NO")
50 | do {
51 | let histories = try context.fetch(fetchRequest)
52 | for history in histories {
53 | context.delete(history)
54 | }
55 | try context.save()
56 | context.refreshAllObjects()
57 | print("All unpinned ClipboardHistory data deleted.")
58 | } catch {
59 | let nsError = error as NSError
60 | print("Error deleting data: \(nsError), \(nsError.userInfo)")
61 | throw nsError
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Clipchop/Clipboard/Clipboard Model/ClipboardModelManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClipboardManager.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/5.
6 | //
7 |
8 | import SwiftUI
9 | import Combine
10 | import CoreData
11 | import Defaults
12 |
13 | class ClipboardModelManager: ObservableObject {
14 | @Published private(set) var items: [ClipboardHistory] = []
15 |
16 | let manager = FolderManager()
17 |
18 | private var cancellables = Set()
19 | private let container: NSPersistentContainer
20 | private let context: NSManagedObjectContext
21 | private var timerCancellable: AnyCancellable?
22 | private var provider = ClipboardDataProvider.shared
23 |
24 | init() {
25 | container = NSPersistentContainer(name: "ClipboardModel")
26 | container.loadPersistentStores { description, error in
27 | if let error = error {
28 | fatalError("Failed to load Core Data stack: \(error)")
29 | }
30 | }
31 | context = container.viewContext
32 | context.automaticallyMergesChangesFromParent = true // Enable automatic merging of changes
33 | }
34 |
35 | func deleteOldHistory(preservationPeriod: HistoryPreservationPeriod, preservationTime: Int) {
36 | guard preservationPeriod != .forever else { return }
37 |
38 | let currentDate = Date()
39 | let cutoffDate: Date
40 |
41 | switch preservationPeriod {
42 | case .minute:
43 | cutoffDate = currentDate.addingTimeInterval(-Double(preservationTime * 60))
44 | case .hour:
45 | cutoffDate = currentDate.addingTimeInterval(-Double(preservationTime * 60 * 60))
46 | case .day:
47 | cutoffDate = currentDate.addingTimeInterval(-Double(preservationTime * 24 * 60 * 60))
48 | case .month:
49 | cutoffDate = Calendar.current.date(byAdding: .month, value: -preservationTime, to: currentDate) ?? currentDate
50 | case .year:
51 | cutoffDate = Calendar.current.date(byAdding: .year, value: -preservationTime, to: currentDate) ?? currentDate
52 | default:
53 | return
54 | }
55 |
56 | let fetchRequest = NSFetchRequest(entityName: "ClipboardHistory")
57 | fetchRequest.predicate = NSPredicate(format: "pin == NO AND time < %@", cutoffDate as NSDate)
58 | fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \ClipboardHistory.time, ascending: false)]
59 |
60 | do {
61 | let items = try context.fetch(fetchRequest)
62 | for item in items {
63 | try deleteItem(item)
64 | }
65 | try context.save()
66 | log(self, "Deleted old history items successfully.")
67 | } catch {
68 | log(self, "Failed to fetch ClipboardHistory: \(error)")
69 | }
70 | }
71 |
72 | func startPeriodicCleanup() {
73 | log(self, "time start, Cached preservation period: \(Defaults[.historyPreservationPeriod]), Cached preservation time: \(Defaults[.historyPreservationTime])")
74 | timerCancellable = Timer.publish(every: 600, on: .main, in: .common)
75 | .autoconnect()
76 | .sink { [weak self] _ in
77 | guard let self = self else { return }
78 | self.deleteOldHistory(
79 | preservationPeriod: Defaults[.historyPreservationPeriod],
80 | preservationTime: Int(Defaults[.historyPreservationTime])
81 | )
82 | }
83 | }
84 |
85 | func stopPeriodicCleanup() {
86 | timerCancellable?.cancel()
87 | timerCancellable = nil
88 | log(self, "time stop, \(Defaults[.historyPreservationPeriod]), \(Defaults[.historyPreservationTime])")
89 | }
90 |
91 | func restartPeriodicCleanup() {
92 | stopPeriodicCleanup()
93 | startPeriodicCleanup()
94 | log(self, "\(Defaults[.historyPreservationPeriod]), \(Defaults[.historyPreservationTime])")
95 | }
96 |
97 | private func deleteItem(_ item: ClipboardHistory) throws {
98 | let context = provider.viewContext
99 | let existingItem = try context.existingObject(with: item.objectID)
100 | context.delete(existingItem)
101 | Task(priority: .background) {
102 | try await context.perform {
103 | try context.save()
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Clipchop/Clipboard/ClipboardManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClipboardModelManager.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/5.
6 | //
7 |
8 | import AppKit
9 | import Defaults
10 | import KeyboardShortcuts
11 |
12 | class ClipboardManager {
13 | static var clipboardController: ClipboardController?
14 |
15 | private let context: NSManagedObjectContext
16 |
17 | let beginningViewController = BeginningViewController()
18 | let clipHistoryViewController = ClipHistoryPanelController()
19 | let clipboardModelManager = ClipboardModelManager()
20 |
21 | init(context: NSManagedObjectContext) {
22 |
23 | self.context = context
24 |
25 | Self.clipboardController = .init(
26 | context: context,
27 | clipHistoryViewController: clipHistoryViewController,
28 | clipboardModelManager: clipboardModelManager
29 | )
30 | Self.clipboardController?.start()
31 |
32 | KeyboardShortcuts.onKeyDown(for: .window) {
33 | let cursorPosition = Defaults[.cursorPosition]
34 |
35 | let position: CGPoint
36 |
37 | switch cursorPosition {
38 | case .mouseLocation:
39 | position = NSEvent.mouseLocation
40 |
41 | case .adjustedPosition:
42 | if let screenCursorPosition = getIMECursorPosition() {
43 | let globalCursorPosition = convertScreenToGlobalCoordinates(screenPoint: screenCursorPosition)
44 | position = CGPoint(x: globalCursorPosition.x - 20, y: globalCursorPosition.y - 30)
45 | } else {
46 | position = NSEvent.mouseLocation
47 | }
48 |
49 | case .fixedPosition:
50 | if let screen = NSScreen.main {
51 | let screenFrame = screen.frame
52 |
53 | let x = screenFrame.midX - (Defaults[.displayMore] ? 350 : 250)
54 |
55 | let y = screenFrame.minY + (Defaults[.displayMore] ? 250 : 200)
56 |
57 | let fixedPosition = CGPoint(x: x, y: y)
58 |
59 | position = fixedPosition
60 | } else {
61 | position = NSEvent.mouseLocation
62 | }
63 | case .custom:
64 | if let windowPosition = UserDefaults.standard.dictionary(forKey: "windowPosition") as? [String: CGFloat] {
65 | let x = windowPosition["x"] ?? 0
66 | let y = windowPosition["y"] ?? 0
67 | let customPosition = CGPoint(x: x, y: y)
68 | position = customPosition
69 | } else {
70 | position = NSEvent.mouseLocation
71 | }
72 | }
73 | self.clipHistoryViewController.toggle(position: position)
74 | }
75 |
76 | KeyboardShortcuts.onKeyDown(for: .start) {
77 | Self.clipboardController?.toggle()
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Clipchop/Clipboard/Data Storage/ClipboardContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClipboardContent.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/5.
6 | //
7 |
8 | import Foundation
9 | import CoreData
10 |
11 | final class ClipboardContent: NSManagedObject {
12 | enum Managed: String {
13 | case type = "type"
14 | case value = "value"
15 | }
16 |
17 | static let entityName = "ClipboardContent"
18 |
19 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
20 | .init(entityName: ClipboardContent.entityName)
21 | }
22 |
23 | @NSManaged public var type: String?
24 | @NSManaged public var value: Data?
25 | @NSManaged public var item: ClipboardHistory?
26 |
27 | convenience init(type: String, value: Data?) {
28 | let entity = NSEntityDescription.entity(
29 | forEntityName: ClipboardContent.entityName,
30 | in: ClipboardDataProvider.shared.viewContext
31 | )!
32 | self.init(entity: entity, insertInto: ClipboardDataProvider.shared.viewContext)
33 |
34 | self.type = type
35 | self.value = value
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Clipchop/Clipboard/Data Storage/ClipboardModel.xcdatamodeld/ClipboardModel.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Clipchop/Clipboard/FolderManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FolderManager.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/8/24.
6 | //
7 |
8 | import Foundation
9 | import Defaults
10 |
11 | class FolderManager {
12 | func addFolder(named folderName: String) {
13 | var folders = Defaults[.folders]
14 | if !folders.contains(where: { $0.name == folderName }) {
15 | let newFolder = Folder(id: UUID(), name: folderName, itemIDs: [])
16 | folders.append(newFolder)
17 | Defaults[.folders] = folders
18 | }
19 | }
20 |
21 | func removeFolder(named folderName: String) {
22 | var folders = Defaults[.folders]
23 | folders.removeAll { $0.name == folderName }
24 | Defaults[.folders] = folders
25 | }
26 |
27 | func renameFolder(from oldName: String, to newName: String) {
28 | var folders = Defaults[.folders]
29 | if let index = folders.firstIndex(where: { $0.name == oldName }) {
30 | folders[index].name = newName
31 | Defaults[.folders] = folders
32 | }
33 | }
34 |
35 | func addItem(_ items: [ClipboardHistory], toFolder named: String) {
36 | var folders = Defaults[.folders]
37 | if let index = folders.firstIndex(where: { $0.name == named }) {
38 | let itemIDs = items.compactMap { $0.id }
39 | folders[index].itemIDs.append(contentsOf: itemIDs)
40 | Defaults[.folders] = folders
41 | }
42 | }
43 |
44 | func removeItem(_ items: [ClipboardHistory], fromFolder named: String) {
45 | var folders = Defaults[.folders]
46 | if let index = folders.firstIndex(where: { $0.name == named }) {
47 | let itemIDs = items.compactMap { $0.id }
48 | folders[index].itemIDs.removeAll { itemIDs.contains($0) }
49 | Defaults[.folders] = folders
50 | }
51 | }
52 |
53 | func items(inFolder named: String) -> [UUID]? {
54 | return Defaults[.folders].first(where: { $0.name == named })?.itemIDs
55 | }
56 |
57 | func allFolders() -> [String] {
58 | return Defaults[.folders]
59 | .map { $0.name }
60 | .sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
61 | }
62 |
63 | func refreshFolders(with existingItems: [ClipboardHistory]) {
64 | var folders = Defaults[.folders]
65 | let existingItemIDs = Set(existingItems.compactMap { $0.id })
66 |
67 | for index in folders.indices {
68 | folders[index].itemIDs.removeAll { !existingItemIDs.contains($0) }
69 | }
70 |
71 | Defaults[.folders] = folders
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Clipchop/Clipchop.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Clipchop/ClipchopApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClipchopApp.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/5.
6 | //
7 |
8 | import SwiftUI
9 | import Defaults
10 | import KeyboardShortcuts
11 | import MenuBarExtraAccess
12 |
13 | let onStreamTime = try! Date("2024-05-13T00:00:00Z", strategy: .iso8601)
14 |
15 | @main
16 | struct ClipchopApp: App {
17 |
18 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
19 |
20 | @State var isMenuBarPresented: Bool = true
21 |
22 | @Default(.menuBarItemEnabled) private var menuBarItemEnabled
23 |
24 | private let clipboardModelEditor = ClipboardModelEditor(provider: .shared)
25 | private let clipHistoryViewController = ClipHistoryPanelController()
26 | private let clipboardManager: ClipboardManager
27 |
28 | init() {
29 |
30 | self.clipboardManager = .init(context: ClipboardDataProvider.shared.viewContext)
31 |
32 | #if DEBUG
33 | // Resets Defaults
34 | Defaults[.menuBarItemEnabled] = Defaults.Keys.menuBarItemEnabled.defaultValue
35 |
36 | // Defaults[.beginningViewShown] = Defaults.Keys.beginningViewShown.defaultValue
37 |
38 | // Times Clipped: 60
39 | // Defaults[.timesClipped] =/* Defaults.Keys.timesClipped.defaultValue*/ 184
40 | // Defaults[.clipSound] = Defaults.Keys.clipSound.defaultValue
41 | // Defaults[.pasteSound] = Defaults.Keys.pasteSound.defaultValue
42 |
43 | // Defaults[.categories] = Defaults.Keys.categories.defaultValue
44 | // Defaults[.allTypes] = Defaults.Keys.allTypes.defaultValue
45 | // Defaults[.folders] = Defaults.Keys.folders.defaultValue
46 |
47 | // Defaults[.excludeAppsEnabled] = Defaults.Keys.excludeAppsEnabled.defaultValue
48 | // Defaults[.excludedApplications] = Defaults.Keys.excludedApplications.defaultValue
49 |
50 | // Defaults[.historyPreservationPeriod] = Defaults.Keys.historyPreservationPeriod.defaultValue
51 | // Defaults[.historyPreservationTime] = Defaults.Keys.historyPreservationTime.defaultValue
52 |
53 | // Resets clipboard history
54 | // try? clipboardModelEditor.deleteAll()
55 | #endif
56 | if !Defaults[.beginningViewShown] {
57 | clipboardManager.beginningViewController.open()
58 | Defaults[.beginningViewShown] = true
59 | } else {
60 | PermissionsManager.requestAccess()
61 | }
62 | }
63 |
64 | var body: some Scene {
65 |
66 | MenuBarExtra("Clipchop", image: "Empty", isInserted: $menuBarItemEnabled) {
67 | MenuBarView(clipboardController: ClipboardManager.clipboardController!)
68 | .environment(\.managedObjectContext, ClipboardDataProvider.shared.viewContext)
69 | }
70 | .menuBarExtraStyle(.menu)
71 | .menuBarExtraAccess(isPresented: $isMenuBarPresented) { menuBarItem in
72 | guard
73 | // Init once
74 | let button = menuBarItem.button,
75 | button.subviews.count == 0
76 | else {
77 | return
78 | }
79 |
80 | menuBarItem.length = 24
81 |
82 | let view = NSHostingView(rootView: MenuBarIconView())
83 | view.frame.size = .init(width: 24, height: NSStatusBar.system.thickness)
84 | button.addSubview(view)
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/AppKit/NSImage+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSImage+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/12.
6 | //
7 |
8 | import AppKit
9 | import SwiftUICore
10 |
11 | extension NSImage {
12 | func resized(to newSize: NSSize) -> NSImage {
13 | return NSImage(size: newSize, flipped: false) { rect in
14 | self.draw(in: rect,
15 | from: NSRect(origin: CGPoint.zero, size: self.size),
16 | operation: NSCompositingOperation.copy,
17 | fraction: 1.0)
18 | return true
19 | }
20 | }
21 | }
22 |
23 | extension NSImage {
24 | var averageColor: Color? {
25 | guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
26 | return nil
27 | }
28 |
29 | let context = CIContext()
30 | let ciImage = CIImage(cgImage: cgImage)
31 | let extent = ciImage.extent
32 | let inputExtent = CIVector(x: extent.origin.x, y: extent.origin.y, z: extent.size.width, w: extent.size.height)
33 | guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: ciImage, kCIInputExtentKey: inputExtent]) else {
34 | return nil
35 | }
36 |
37 | guard let outputImage = filter.outputImage else {
38 | return nil
39 | }
40 |
41 | var bitmap = [UInt8](repeating: 0, count: 4)
42 | context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: CGColorSpaceCreateDeviceRGB())
43 |
44 | return Color(red: Double(bitmap[0]) / 255.0, green: Double(bitmap[1]) / 255.0, blue: Double(bitmap[2]) / 255.0, opacity: Double(bitmap[3]) / 255.0)
45 | }
46 | }
47 |
48 | extension NSImage {
49 | func pngData() -> Data? {
50 | guard let tiffRepresentation = self.tiffRepresentation else { return nil }
51 | guard let bitmap = NSBitmapImageRep(data: tiffRepresentation) else { return nil }
52 | return bitmap.representation(using: .png, properties: [:])
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/AppKit/NSPasteboard.PasteboardType+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSPasteboard.PasteboardType+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/4/28.
6 | //
7 |
8 | import AppKit
9 |
10 | // Code from https://github.com/p0deje/Maccy
11 | extension NSPasteboard.PasteboardType {
12 | static let jpeg = NSPasteboard.PasteboardType(rawValue: "public.jpeg")
13 | static let universalClipboard = NSPasteboard.PasteboardType(rawValue: "com.apple.is-remote-clipboard")
14 |
15 | // See http://nspasteboard.org for more details.
16 | static let autoGenerated = NSPasteboard.PasteboardType(rawValue: "org.nspasteboard.AutoGeneratedType")
17 | static let concealed = NSPasteboard.PasteboardType(rawValue: "org.nspasteboard.ConcealedType")
18 | static let transient = NSPasteboard.PasteboardType(rawValue: "org.nspasteboard.TransientType")
19 |
20 | // https://github.com/p0deje/Maccy/issues/429#issuecomment-1182575226
21 | static let modified = NSPasteboard.PasteboardType(rawValue: "x.nspasteboard.ModifiedType")
22 |
23 | // Types that indicate Microsoft Word bookmarks (links).
24 | // https://github.com/p0deje/Maccy/issues/613
25 | static let microsoftObjectLink = NSPasteboard.PasteboardType(rawValue: "com.microsoft.ObjectLink")
26 | static let microsoftLinkSource = NSPasteboard.PasteboardType(rawValue: "com.microsoft.Link-Source")
27 |
28 | }
29 |
30 | extension NSPasteboard.PasteboardType {
31 | static let fromClipchop = NSPasteboard.PasteboardType(rawValue: "labs.cement.Clipchop")
32 | static let avif = NSPasteboard.PasteboardType(rawValue: "public.avif")
33 | static let appleFinalCutPro = NSPasteboard.PasteboardType(rawValue: "com.apple.flexo.proFFPasteboardUTI")
34 | static let appleReminders = NSPasteboard.PasteboardType(rawValue: "com.apple.reminders.reminderCopyPaste")
35 | }
36 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/AppKit/NSWindow+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSWindow+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/25.
6 | //
7 |
8 | import AppKit
9 |
10 | extension NSWindow {
11 | var standardWindowButtons: [ButtonType: NSButton?] {
12 | [
13 | .closeButton: standardWindowButton(.closeButton),
14 | .documentIconButton: standardWindowButton(.documentIconButton),
15 | .documentVersionsButton: standardWindowButton(.documentVersionsButton),
16 | .miniaturizeButton: standardWindowButton(.miniaturizeButton),
17 | .toolbarButton: standardWindowButton(.toolbarButton),
18 | .zoomButton: standardWindowButton(.zoomButton)
19 | ]
20 | }
21 |
22 | var availableStandardWindowButtons: [ButtonType: NSButton] {
23 | standardWindowButtons
24 | .compactMapValues { $0 }
25 | }
26 |
27 | var visibleWindowButtonTypes: [ButtonType] {
28 | get {
29 | availableStandardWindowButtons
30 | .filter { !$0.value.isHidden }
31 | .keys
32 | .map { $0 }
33 | }
34 |
35 | set {
36 | availableStandardWindowButtons.forEach { entry in
37 | entry.value.isHidden = !newValue.contains([entry.key])
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/Color+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/6.
6 | //
7 |
8 | import SwiftUI
9 | import Defaults
10 | import AppKit
11 | import CoreImage
12 |
13 | extension NSImage {
14 | func dominantColor() -> NSColor? {
15 | guard let inputImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil).flatMap({ CIImage(cgImage: $0) }) else { return nil }
16 |
17 | let extentVector = CIVector(x: inputImage.extent.origin.x,
18 | y: inputImage.extent.origin.y,
19 | z: inputImage.extent.size.width,
20 | w: inputImage.extent.size.height)
21 |
22 | guard let filter = CIFilter(name: "CIAreaAverage", parameters: [
23 | kCIInputImageKey: inputImage,
24 | kCIInputExtentKey: extentVector
25 | ]) else { return nil }
26 |
27 | guard let outputImage = filter.outputImage else { return nil }
28 |
29 | var bitmap = [UInt8](repeating: 0, count: 4)
30 | let context = CIContext(options: [.workingColorSpace: kCFNull!])
31 | context.render(outputImage,
32 | toBitmap: &bitmap,
33 | rowBytes: 4,
34 | bounds: CGRect(x: 0, y: 0, width: 1, height: 1),
35 | format: .RGBA8,
36 | colorSpace: nil)
37 |
38 | return NSColor(red: CGFloat(bitmap[0]) / 255.0,
39 | green: CGFloat(bitmap[1]) / 255.0,
40 | blue: CGFloat(bitmap[2]) / 255.0,
41 | alpha: CGFloat(bitmap[3]) / 255.0)
42 | }
43 | }
44 |
45 | extension Color {
46 | static func getAccent() -> Color {
47 | return inlineAccentColor(style: Defaults[.colorStyle], customColor: Defaults[.customAccentColor])
48 | }
49 |
50 | static func inlineAccentColor(style: ColorStyle, customColor: Color) -> Color {
51 | switch style {
52 | case .app:
53 | return .accent
54 | case .system:
55 | return .blue
56 | case .custom:
57 | return customColor
58 | }
59 | }
60 | }
61 |
62 | extension Color {
63 | var brightness: Double {
64 | guard let components = self.cgColor?.components, components.count >= 3 else {
65 | return 0
66 | }
67 | let red = Double(components[0] * 255)
68 | let green = Double(components[1] * 255)
69 | let blue = Double(components[2] * 255)
70 | return (red * 299 + green * 587 + blue * 114) / 1000
71 | }
72 |
73 | var isLight: Bool {
74 | return brightness > 128
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/Foundation/Bundle+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bundle+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/4/27.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Bundle {
11 | var appName: String { getInfo("CFBundleName") }
12 | var displayName: String { getInfo("CFBundleDisplayName") }
13 | var bundleID: String { getInfo("CFBundleIdentifier") }
14 | var copyright: String { getInfo("NSHumanReadableCopyright") }
15 |
16 | var appBuild: String { getInfo("CFBundleVersion") }
17 | var appVersion: String { getInfo("CFBundleShortVersionString") }
18 |
19 | func getInfo(_ str: String) -> String {
20 | infoDictionary?[str] as? String ?? "⚠️"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/Foundation/MutableCollection+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MutableCollection+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/26.
6 | //
7 |
8 | import Foundation
9 |
10 | extension MutableCollection {
11 | mutating func updateEach(_ update: (inout Element) -> Void) {
12 | for i in indices {
13 | update(&self[i])
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/Foundation/Notification+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Notification+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/25.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Notification.Name {
11 | static let didClip = Self("didClip")
12 | static let didPaste = Self("didPaste")
13 | // ClipHistory View expansion notification
14 | }
15 |
16 | extension Notification.Name {
17 | @discardableResult
18 | func onReceive(object: Any? = nil, using: @escaping (Notification) -> Void) -> NSObjectProtocol {
19 | return NotificationCenter.default.addObserver(
20 | forName: self,
21 | object: object,
22 | queue: .main,
23 | using: using
24 | )
25 | }
26 |
27 | func post(object: Any? = nil, userInfo: [AnyHashable: Any]? = nil) {
28 | NotificationCenter.default.post(name: self, object: object, userInfo: userInfo)
29 | }
30 | }
31 |
32 | extension Notification.Name {
33 | static let panelDidClose = Notification.Name("panelDidClose")
34 | static let panelDidOpen = Notification.Name("panelDidOpen")
35 | static let panelDidLogout = Notification.Name("panelDidLogout")
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/Foundation/Set+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Set+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/28.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Set {
11 | mutating func updateEach(_ update: (inout Element) -> Void) {
12 | for i in indices {
13 | let original = self[i]
14 | var element = original
15 | update(&element)
16 |
17 | guard original != element else { continue }
18 | self.remove(original)
19 | self.insert(element)
20 | }
21 | }
22 | }
23 |
24 | extension Set where Element == FileType {
25 | func sorted() -> [FileType] {
26 | sorted {
27 | $0.ext < $1.ext
28 | }
29 | }
30 | }
31 |
32 | extension Set where Element == FileType.Category {
33 | func sorted() -> [FileType.Category] {
34 | sorted {
35 | $0.name < $1.name
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/KeyboardShortcuts+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardShortcuts+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/4/28.
6 | //
7 |
8 | import KeyboardShortcuts
9 | import AppKit
10 |
11 | extension KeyboardShortcuts.Name {
12 |
13 | static let window = Self("window", default: .init(.w, modifiers: .option))
14 | static let start = Self("start", default: .init(.q, modifiers: .control))
15 |
16 | static let settings = Self("settings", default: .init(.comma, modifiers: .command))
17 | // static let close = Self("close", default: .init(.w, modifiers: .command))
18 | static let expand = Self("expand", default: .init(.rightBracket))
19 | static let collapse = Self("collapse", default: .init(.leftBracket))
20 | }
21 |
22 | extension KeyboardShortcuts.Key {
23 | var shortcut: KeyboardShortcuts.Shortcut {
24 | .init(self)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/SwiftUI/EnvironmentValues+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnvironmentValues+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension EnvironmentValues {
11 | var hasTitle: Bool {
12 | get { self[HasTitleEnvironmentKey.self] }
13 | set { self[HasTitleEnvironmentKey.self] = newValue }
14 | }
15 |
16 | var isSearchable: Bool {
17 | get { self[IsSearchableEnvironmentKey.self] }
18 | set { self[IsSearchableEnvironmentKey.self] = newValue }
19 | }
20 |
21 | var alternatingLayout: Bool {
22 | get { self[AlternatingLayourEnvironmentKey.self] }
23 | set { self[AlternatingLayourEnvironmentKey.self] = newValue }
24 | }
25 |
26 | var canContinue: (Bool) -> Void {
27 | get { self[CanContinueEnvironmentKey.self] }
28 | set { self[CanContinueEnvironmentKey.self] = newValue }
29 | }
30 |
31 | var viewController: ViewController? {
32 | get { self[ViewControllerEnvironmentKey.self] }
33 | set { self[ViewControllerEnvironmentKey.self] = newValue }
34 | }
35 |
36 | var namespace: Namespace.ID? {
37 | get { self[NamespaceEnvironmentKey.self] }
38 | set { self[NamespaceEnvironmentKey.self] = newValue }
39 | }
40 |
41 | var isVisible: Bool {
42 | get { self[IsVisibleEnvironmentKey.self] }
43 | set { self[IsVisibleEnvironmentKey.self] = newValue }
44 | }
45 | }
46 |
47 | struct HasTitleEnvironmentKey: EnvironmentKey {
48 | static var defaultValue: Bool = true
49 | }
50 |
51 | struct IsSearchableEnvironmentKey: EnvironmentKey {
52 | static var defaultValue: Bool = true
53 | }
54 |
55 | struct AlternatingLayourEnvironmentKey: EnvironmentKey {
56 | static var defaultValue: Bool = false
57 | }
58 |
59 | struct CanContinueEnvironmentKey: EnvironmentKey {
60 | static var defaultValue: (Bool) -> Void = { _ in }
61 | }
62 |
63 | struct ViewControllerEnvironmentKey: EnvironmentKey {
64 | static var defaultValue: ViewController?
65 | }
66 |
67 | struct NamespaceEnvironmentKey: EnvironmentKey {
68 | static var defaultValue: Namespace.ID?
69 | }
70 |
71 | struct IsVisibleEnvironmentKey: EnvironmentKey {
72 | static var defaultValue: Bool = true
73 | }
74 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/SwiftUI/Transision+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Transision+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // https://stackoverflow.com/a/71599594/23452915
11 | extension AnyTransition {
12 | static func rotate3D(angle: Angle) -> AnyTransition {
13 | AnyTransition.modifier(
14 | active: Rotate3DModifier(value: 1, angle: angle),
15 | identity: Rotate3DModifier(value: 0, angle: angle))
16 | }
17 | }
18 |
19 | struct Rotate3DModifier: ViewModifier {
20 | let value: Double
21 | let angle: Angle
22 |
23 | func body(content: Content) -> some View {
24 | content
25 | .scaleEffect(1 - value * 0.25)
26 | .rotation3DEffect(.radians(angle.radians * value), axis: (x: 0, y: 1, z: 0))
27 | .opacity(1 - value)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/SwiftUI/View+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+Extensions.swift
3 | // Dial
4 | //
5 | // Created by KrLite on 2024/3/23.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftUIIntrospect
10 |
11 | extension View {
12 | func or(_ condition: Bool, _ another: () -> Self) -> Self {
13 | condition ? another() : self
14 | }
15 |
16 | @ViewBuilder
17 | func orSomeView(_ condition: Bool, _ another: () -> some View) -> some View {
18 | if condition {
19 | another()
20 | } else {
21 | self
22 | }
23 | }
24 |
25 | @ViewBuilder
26 | func `if`(
27 | _ condition: Bool,
28 | trueExpression: (Self) -> some View,
29 | falseExpression: (Self) -> some View
30 | ) -> some View {
31 | if condition {
32 | trueExpression(self)
33 | } else {
34 | falseExpression(self)
35 | }
36 | }
37 |
38 | @ViewBuilder
39 | func `if`(
40 | _ condition: Bool,
41 | expression: (Self) -> some View
42 | ) -> some View {
43 | `if`(condition, trueExpression: expression) { view in
44 | view
45 | }
46 | }
47 |
48 | @ViewBuilder
49 | func possibleKeyboardShortcut(
50 | _ key: KeyEquivalent?,
51 | modifiers: EventModifiers = .command,
52 | localization: KeyboardShortcut.Localization = .automatic
53 | ) -> some View {
54 | if let key {
55 | self.keyboardShortcut(key, modifiers: modifiers, localization: localization)
56 | } else {
57 | self
58 | }
59 | }
60 |
61 | // https://github.com/MrKai77/Loop
62 | func onReceive(
63 | _ name: Notification.Name,
64 | center: NotificationCenter = .default,
65 | object: AnyObject? = nil,
66 | perform action: @escaping (Notification) -> Void
67 | ) -> some View {
68 | self.onReceive(
69 | center.publisher(for: name, object: object),
70 | perform: action
71 | )
72 | }
73 |
74 | @ViewBuilder
75 | func navigationSplitViewCollapsingDisabled() -> some View {
76 | // Completely prevents the sidebar from collapsing
77 | self.introspect(.navigationSplitView, on: .macOS(.v14), scope: .ancestor) { splitView in
78 | (splitView.delegate as? NSSplitViewController)?.splitViewItems.forEach { $0.canCollapse = false }
79 | }
80 | }
81 | }
82 |
83 | extension View {
84 | func log(_ items: Any..., separator: String = " ", terminator: String = "\n") {
85 | Clipchop.log(self, items, separator: separator, terminator: terminator)
86 | }
87 | }
88 |
89 | extension View {
90 | @ViewBuilder
91 | func applyMatchedGeometryEffect(if condition: Bool, id: AnyHashable, namespace: Namespace.ID) -> some View {
92 | if condition {
93 | self.matchedGeometryEffect(id: id, in: namespace, properties: .frame, anchor: .center, isSource: true)
94 | } else {
95 | self
96 | }
97 | }
98 | }
99 |
100 |
101 | extension View {
102 | func applyKeyboardShortcut(_ keyboardShortcut: String, modifier: EventModifiers) -> some View {
103 | if keyboardShortcut != "none" && !modifier.isEmpty {
104 | return AnyView(self.keyboardShortcut(KeyEquivalent(keyboardShortcut.first!), modifiers: modifier))
105 | } else {
106 | return AnyView(self)
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/SwiftUI/View+Functions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+Functions.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @ViewBuilder
11 | func description(@ViewBuilder label: () -> some View) -> some View {
12 | label()
13 | .font(.caption)
14 | .foregroundStyle(.secondary)
15 | }
16 |
17 | @ViewBuilder
18 | func withCaption(
19 | condition: Bool = true,
20 | spacing: CGFloat? = nil,
21 | @ViewBuilder content: () -> some View,
22 | @ViewBuilder caption: () -> some View
23 | ) -> some View {
24 | VStack(alignment: .leading, spacing: spacing) {
25 | content()
26 |
27 | if condition {
28 | description {
29 | caption()
30 | }
31 | }
32 | }
33 | }
34 |
35 | @ViewBuilder
36 | func withCaption(
37 | _ descriptionKey: LocalizedStringKey,
38 | condition: Bool = true,
39 | spacing: CGFloat? = nil,
40 | @ViewBuilder content: () -> some View
41 | ) -> some View {
42 | withCaption(condition: condition, spacing: spacing) {
43 | content()
44 | } caption: {
45 | Text(descriptionKey)
46 | }
47 | }
48 |
49 | @ViewBuilder
50 | func previewSection(content: () -> some View) -> some View {
51 | previewPage {
52 | Form {
53 | content()
54 | }
55 | }
56 | }
57 |
58 | @ViewBuilder
59 | func previewPage(content: () -> some View) -> some View {
60 | content()
61 | .formStyle(.grouped)
62 | }
63 |
--------------------------------------------------------------------------------
/Clipchop/Extensions/UNNotification+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UNNotification+Extensions.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/8/13.
6 | //
7 |
8 |
9 | import SwiftUI
10 | import UserNotifications
11 |
12 | // Thanks https://stackoverflow.com/questions/45226847/unnotificationattachment-failing-to-attach-image
13 | extension UNNotificationAttachment {
14 | static func create(_ imgData: NSData) -> UNNotificationAttachment? {
15 | let imageFileIdentifier = UUID().uuidString + ".jpeg"
16 |
17 | let fileManager = FileManager.default
18 | let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
19 | let tmpSubFolderURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(
20 | tmpSubFolderName,
21 | isDirectory: true
22 | )
23 |
24 | do {
25 | try fileManager.createDirectory(at: tmpSubFolderURL!, withIntermediateDirectories: true, attributes: nil)
26 | let fileURL = tmpSubFolderURL?.appendingPathComponent(imageFileIdentifier)
27 | try imgData.write(to: fileURL!, options: [])
28 | let imageAttachment = try UNNotificationAttachment(
29 | identifier: imageFileIdentifier,
30 | url: fileURL!,
31 | options: nil
32 | )
33 | return imageAttachment
34 | } catch {
35 | print("error \(error.localizedDescription)")
36 | }
37 |
38 | return nil
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Clipchop/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Clipchop/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Clipchop/Utilities/DefaultsStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultsStack.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/18.
6 | //
7 |
8 | import Foundation
9 | import Defaults
10 |
11 | extension [AnyHashable] {
12 | var combinedHashValue: Int {
13 | var hasher = Hasher()
14 | self.forEach {
15 | hasher.combine($0)
16 | }
17 | return hasher.finalize()
18 | }
19 | }
20 |
21 | struct DefaultsStack {
22 | enum Group: String, CaseIterable {
23 | case accentColor = "accentColor"
24 | case historyPreservation = "historyPreservation"
25 |
26 | var relationships: [AnyHashable] {
27 | switch self {
28 | case .accentColor:
29 | [Defaults[.colorStyle], Defaults[.customAccentColor]]
30 | case .historyPreservation:
31 | [Defaults[.historyPreservationPeriod], Defaults[.historyPreservationTime]]
32 | }
33 | }
34 |
35 | var hashValue: Int {
36 | relationships.combinedHashValue
37 | }
38 |
39 | var isUnchanged: Bool {
40 | DefaultsStack.shared.isUnchanged(self)
41 | }
42 |
43 | func isIdentical(comparedTo: [AnyHashable]) -> Bool {
44 | DefaultsStack.shared.isIdentical(self, comparedTo: comparedTo)
45 | }
46 |
47 | func markDirty() {
48 | DefaultsStack.shared.markDirty(self)
49 | }
50 | }
51 |
52 | static var shared = DefaultsStack()
53 |
54 | var hash: [Group: Int]
55 |
56 | init() {
57 | hash = DefaultsStack.hashAll()
58 | }
59 |
60 | static func hashAll(_ groups: [Group] = Group.allCases) -> [Group: Int] {
61 | groups.reduce(into: [Group: Int]()) { result, group in
62 | result[group] = group.hashValue
63 | }
64 | }
65 |
66 | func isUnchanged(_ group: Group) -> Bool {
67 | guard let hashed = hash[group] else { return false }
68 | return hashed == group.hashValue
69 | }
70 |
71 | func isIdentical(_ group: Group, comparedTo: [AnyHashable]) -> Bool {
72 | group.hashValue == comparedTo.combinedHashValue
73 | }
74 |
75 | mutating func markDirty(_ group: Group) {
76 | hash[group] = group.hashValue
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Clipchop/Utilities/LifecycleHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LifecycleHandler.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/25.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | func quit() {
12 | NSApp.terminate(nil)
13 | }
14 |
15 | // https://stackoverflow.com/questions/29847611/restarting-osx-app-programmatically
16 | func relaunch() {
17 | let url = URL(fileURLWithPath: Bundle.main.resourcePath!)
18 | let path = url.deletingLastPathComponent().deletingLastPathComponent().absoluteString
19 | let task = Process()
20 |
21 | task.launchPath = "/usr/bin/open"
22 | task.arguments = [path]
23 | task.launch()
24 |
25 | quit()
26 | }
27 |
--------------------------------------------------------------------------------
/Clipchop/Utilities/Logger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/25.
6 | //
7 |
8 | func log(
9 | _ subject: Subject? = nil,
10 | _ items: Any..., separator: String = " ", terminator: String = "\n"
11 | ) {
12 | if let subject {
13 | print(String(describing: type(of: subject)), terminator: " - ")
14 | }
15 | print(items, separator: separator, terminator: terminator)
16 | }
17 |
--------------------------------------------------------------------------------
/Clipchop/Utilities/NotificationManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationManager.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/4/28.
6 | //
7 |
8 | import Foundation
9 | import AppKit
10 | import UserNotifications
11 |
12 | class NotificationManager {
13 | private static var center: UNUserNotificationCenter { UNUserNotificationCenter.current() }
14 |
15 | static func authorize() {
16 | center.requestAuthorization(options: [.alert, .sound]) { _, error in
17 | if error != nil {
18 | NSLog("Failed to authorize notifications: \(String(describing: error))")
19 | }
20 | }
21 | }
22 |
23 | static func notify(body: String?, sound: NSSound?) {
24 | guard let body else { return }
25 |
26 | authorize()
27 |
28 | center.getNotificationSettings { settings in
29 | guard (settings.authorizationStatus == .authorized) ||
30 | (settings.authorizationStatus == .provisional) else { return }
31 |
32 | let content = UNMutableNotificationContent()
33 | if settings.alertSetting == .enabled {
34 | content.body = body
35 | }
36 |
37 | let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
38 | center.add(request) { error in
39 | if error != nil {
40 | NSLog("Failed to deliver notification: \(String(describing: error))")
41 | } else {
42 | if settings.soundSetting == .enabled {
43 | sound?.play()
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/Clipchop/Utilities/ObservationTrackingStream.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObservationTrackingStream.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/18.
6 | //
7 |
8 | import Foundation
9 |
10 | func observationTrackingStream(
11 | _ apply: @escaping () -> T
12 | ) -> AsyncStream {
13 | .init { continuation in
14 | @Sendable func observe() {
15 | let result = withObservationTracking {
16 | apply()
17 | } onChange: {
18 | DispatchQueue.main.async {
19 | observe()
20 | }
21 | }
22 |
23 | continuation.yield(result)
24 | }
25 |
26 | observe()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Clipchop/Utilities/PerimissionsManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PerimissionsManager.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/4/27.
6 | //
7 |
8 | import Foundation
9 | import AppKit
10 | import FullDiskAccess
11 |
12 | class PermissionsManager {
13 | static var remaining: Int {
14 | [Accessibility.getStatus(), FullDisk.getStatus()]
15 | .filter { !$0 }
16 | .count
17 | }
18 |
19 | static func requestAccess() {
20 | PermissionsManager.Accessibility.requestAccess()
21 | PermissionsManager.FullDisk.requestAccess()
22 | }
23 |
24 | class Accessibility {
25 | static func getStatus() -> Bool {
26 | // Get current status for Accessibility Access
27 | let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: false]
28 | let status = AXIsProcessTrustedWithOptions(options)
29 |
30 | return status
31 | }
32 |
33 | @discardableResult
34 | static func requestAccess() -> Bool {
35 | // More information on this behaviour: https://stackoverflow.com/questions/29006379/accessibility-permissions-reset-after-application-update
36 | guard !Accessibility.getStatus() else { return true }
37 |
38 | let alert = NSAlert()
39 | alert.alertStyle = NSAlert.Style.informational
40 | alert.messageText = String(
41 | format: String(
42 | localized: "Accessibility Access Alert: Title",
43 | defaultValue: "%@ Needs Accessibility Access"
44 | ),
45 | Bundle.main.appName
46 | )
47 | alert.informativeText = String(
48 | format: String(
49 | localized: "Accessibility Access Alert: Content",
50 | defaultValue: """
51 | Accessibility Access is required for %@ to take over your clipboard.
52 | """
53 | ),
54 | Bundle.main.appName
55 | )
56 |
57 | alert.runModal()
58 |
59 | let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
60 | let status = AXIsProcessTrustedWithOptions(options)
61 |
62 | return status
63 | }
64 | }
65 |
66 | class FullDisk {
67 | static func getStatus() -> Bool {
68 | // Get current status for Full Disk Access
69 | return FullDiskAccess.isGranted
70 | }
71 |
72 | @discardableResult
73 | static func requestAccess() -> Bool {
74 | guard !FullDisk.getStatus() else { return true }
75 |
76 | FullDiskAccess.promptIfNotGranted(
77 | title: String(
78 | format: String(
79 | localized: "Full Disk Access Alert: Title",
80 | defaultValue: "%@ Needs Full Disk Access"
81 | ),
82 | Bundle.main.appName
83 | ),
84 | message: String(
85 | format: String(
86 | localized: "Full Disk Access Alert: Content",
87 | defaultValue: """
88 | Full Disk Access is required for %@ to generate file previews.
89 | """
90 | ),
91 | Bundle.main.appName
92 | ),
93 | settingsButtonTitle: String(localized: .init("Open in System Settings", defaultValue: "Open in System Settings")),
94 | skipButtonTitle: String(localized: .init("Later", defaultValue: "Later")),
95 | canBeSuppressed: false,
96 | icon: nil
97 | )
98 |
99 | return getStatus()
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Clipchop/Utilities/Throttler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Throttler.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/20.
6 | //
7 |
8 | import Foundation
9 |
10 | class Throttler {
11 | var minimumDelay: TimeInterval
12 |
13 | private var workItem: DispatchWorkItem = DispatchWorkItem(block: {})
14 | private var previousRun: Date = Date.distantPast
15 | private let queue: DispatchQueue
16 |
17 | init(minimumDelay: TimeInterval, queue: DispatchQueue = DispatchQueue.main) {
18 | self.minimumDelay = minimumDelay
19 | self.queue = queue
20 | }
21 |
22 | func throttle(_ block: @escaping () -> Void) {
23 | cancel()
24 | workItem = DispatchWorkItem { [weak self] in
25 | self?.previousRun = Date()
26 | block()
27 | }
28 |
29 | let delay = previousRun.timeIntervalSinceNow > minimumDelay ? 0 : minimumDelay
30 | queue.asyncAfter(deadline: .now() + Double(delay), execute: workItem)
31 | }
32 |
33 | func cancel() {
34 | workItem.cancel()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Clipchop/Utilities/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/25.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ViewController {
11 | func close()
12 | }
13 |
--------------------------------------------------------------------------------
/Clipchop/Utilities/getCursorPosition.swift:
--------------------------------------------------------------------------------
1 | //
2 | // getCursorPosition.swift
3 | // Clipchop
4 | //
5 | // Created by Xinshao_Air on 2024/7/19.
6 | //
7 |
8 | import Foundation
9 | import ApplicationServices
10 | import Accessibility
11 | import Carbon
12 | import AppKit
13 |
14 | func getFrontmostAppPID() -> pid_t? {
15 | guard let frontmostApp = NSWorkspace.shared.frontmostApplication else {
16 | return nil
17 | }
18 | return frontmostApp.processIdentifier
19 | }
20 |
21 | func getIMECursorPosition() -> CGPoint? {
22 | guard let frontmostAppPID = getFrontmostAppPID() else {
23 | print("Failed to get frontmost app PID")
24 | return nil
25 | }
26 |
27 | let app = AXUIElementCreateApplication(frontmostAppPID)
28 | var focusedElement: CFTypeRef?
29 | let result = AXUIElementCopyAttributeValue(app, kAXFocusedUIElementAttribute as CFString, &focusedElement)
30 |
31 | if result != .success {
32 | print("Failed to get focused element, result: \(result)")
33 | return nil
34 | }
35 |
36 | guard let focusedElement = focusedElement else {
37 | print("Focused element is nil")
38 | return nil
39 | }
40 |
41 | var rangeValue: CFTypeRef?
42 | let rangeResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextRangeAttribute as CFString, &rangeValue)
43 |
44 | if rangeResult != .success {
45 | print("Failed to get selected text range, result: \(rangeResult)")
46 | return nil
47 | }
48 |
49 | guard let rangeValue = rangeValue else {
50 | print("Selected text range is nil")
51 | return nil
52 | }
53 |
54 | var range = CFRange()
55 | if !AXValueGetValue(rangeValue as! AXValue, .cfRange, &range) {
56 | print("Failed to convert AXValue to CFRange")
57 | return nil
58 | }
59 |
60 | var boundsValue: CFTypeRef?
61 | let boundsResult = AXUIElementCopyParameterizedAttributeValue(focusedElement as! AXUIElement, kAXBoundsForRangeParameterizedAttribute as CFString, rangeValue, &boundsValue)
62 |
63 | if boundsResult != .success {
64 | print("Failed to get bounds for range, result: \(boundsResult)")
65 | return nil
66 | }
67 |
68 | guard let boundsValue = boundsValue else {
69 | print("Bounds value is nil")
70 | return nil
71 | }
72 |
73 | var rect = CGRect()
74 | if !AXValueGetValue(boundsValue as! AXValue, .cgRect, &rect) {
75 | print("Failed to convert AXValue to CGRect")
76 | return nil
77 | }
78 |
79 | return rect.origin
80 | }
81 |
82 | func convertScreenToGlobalCoordinates(screenPoint: CGPoint) -> CGPoint {
83 | let screenHeight = NSScreen.main?.frame.height ?? 0
84 | let convertedY = screenHeight - screenPoint.y
85 | return CGPoint(x: screenPoint.x, y: convertedY)
86 | }
87 |
88 | func getGlobalCursorPosition() -> CGPoint {
89 | if let screenCursorPosition = getIMECursorPosition() {
90 | return convertScreenToGlobalCoordinates(screenPoint: screenCursorPosition)
91 | } else {
92 | return NSEvent.mouseLocation
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Clipchop/Views/Beginning/BeginningViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BeginningViewController.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/22.
6 | //
7 |
8 | import SwiftUI
9 | import Defaults
10 | import KeyboardShortcuts
11 |
12 | @Observable class BeginningViewController: ViewController {
13 | static let size = NSSize(width: 450, height: 500)
14 |
15 | private var windowController: NSWindowController?
16 |
17 | // MARK: - Open / Close
18 |
19 | var isOpened: Bool {
20 | windowController != nil
21 | }
22 |
23 | func open() {
24 | if let windowController {
25 | windowController.window?.orderFrontRegardless()
26 | return
27 | }
28 |
29 | let window = NSWindow(
30 | contentRect: .zero,
31 | styleMask: [.closable, .titled, .fullSizeContentView],
32 | backing: .buffered,
33 | defer: true
34 | )
35 |
36 | let screenRect = NSScreen.main?.frame ?? .null
37 |
38 | window.titlebarAppearsTransparent = true
39 | window.titleVisibility = .hidden
40 | window.collectionBehavior = .canJoinAllSpaces
41 | window.level = .normal
42 | window.isMovableByWindowBackground = true
43 |
44 | window.visibleWindowButtonTypes = [.closeButton]
45 |
46 | window.contentView = NSHostingView(
47 | rootView: BeginningView()
48 | .environment(\.viewController, self)
49 | )
50 | window.setFrame(CGRect(
51 | x: screenRect.midX - Self.size.width / 2, y: screenRect.midY - Self.size.height / 2,
52 | width: Self.size.width, height: Self.size.height
53 | ), display: false)
54 | window.orderFrontRegardless()
55 |
56 | windowController = .init(window: window)
57 | }
58 |
59 | func close() {
60 | guard let windowController else { return }
61 | self.windowController = nil
62 | windowController.close()
63 | }
64 |
65 | func toggle() {
66 | if isOpened {
67 | close()
68 | } else {
69 | open()
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Clipchop/Views/Beginning/Pages/BeginningAllSetPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BeginningAllSetPage.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BeginningAllSetPage: View {
11 | @Environment(\.namespace) private var namespace
12 | @Environment(\.isVisible) private var isVisible
13 |
14 | var body: some View {
15 | VStack {
16 | if isVisible {
17 | Image(nsImage: AppIcon.currentAppIcon.image)
18 | .matchedGeometryEffect(id: "flip", in: namespace!)
19 | .transition(.blurReplace.combined(with: .scale(0.75)))
20 | }
21 |
22 | Text("You're All Set!").foregroundStyle(.accent)
23 | }
24 | .font(.title)
25 | .bold()
26 | .frame(width: BeginningViewController.size.width)
27 | .frame(maxHeight: .infinity)
28 | }
29 | }
30 |
31 | #Preview {
32 | BeginningAllSetPage()
33 | }
34 |
--------------------------------------------------------------------------------
/Clipchop/Views/Beginning/Pages/BeginningCustomizationPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BeginningCustomizationPage.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BeginningCustomizationPage: View {
11 | @Environment(\.namespace) private var namespace
12 | @Environment(\.isVisible) private var isVisible
13 |
14 | var body: some View {
15 | VStack {
16 | VStack {
17 | if isVisible {
18 | Image(systemSymbol: .pencilAndOutline)
19 | .imageScale(.large)
20 | .padding()
21 | .matchedGeometryEffect(id: "flip", in: namespace!)
22 | .transition(.rotate3D(angle: .degrees(65)).combined(with: .scale).combined(with: .opacity))
23 | }
24 |
25 | Text("Customization")
26 | }
27 | .font(.title)
28 | .bold()
29 | .frame(height: 200)
30 |
31 | Form {
32 | AppearanceSection()
33 | }
34 | .formStyle(.grouped)
35 | .scrollDisabled(true)
36 | }
37 | .padding()
38 | .frame(width: BeginningViewController.size.width)
39 | .frame(maxHeight: .infinity)
40 | }
41 | }
42 |
43 | #Preview {
44 | BeginningCustomizationPage()
45 | }
46 |
--------------------------------------------------------------------------------
/Clipchop/Views/Beginning/Pages/BeginningHelloPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BeginningHelloPage.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BeginningHelloPage: View {
11 | @Environment(\.namespace) private var namespace
12 | @Environment(\.isVisible) private var isVisible
13 |
14 | var body: some View {
15 | VStack {
16 | if isVisible {
17 | Image(nsImage: AppIcon.currentAppIcon.image)
18 | .matchedGeometryEffect(id: "flip", in: namespace!)
19 | .transition(.blurReplace.combined(with: .scale(0.75)))
20 | }
21 |
22 | Text("Welcome to \(Text(Bundle.main.appName).foregroundStyle(.accent))")
23 | }
24 | .font(.title)
25 | .bold()
26 | .frame(width: BeginningViewController.size.width)
27 | .frame(maxHeight: .infinity)
28 | }
29 | }
30 |
31 | #Preview {
32 | BeginningHelloPage()
33 | }
34 |
--------------------------------------------------------------------------------
/Clipchop/Views/Beginning/Pages/BeginningPermissionsPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BeginningPermissionsPage.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BeginningPermissionsPage: View {
11 | @Environment(\.canContinue) private var canContinue
12 | @Environment(\.namespace) private var namespace
13 | @Environment(\.isVisible) private var isVisible
14 |
15 | var body: some View {
16 | VStack {
17 | VStack {
18 | if isVisible {
19 | Image(systemSymbol: .lock)
20 | .imageScale(.large)
21 | .padding()
22 | .matchedGeometryEffect(id: "flip", in: namespace!)
23 | .transition(.rotate3D(angle: .degrees(65)).combined(with: .scale).combined(with: .opacity))
24 | }
25 |
26 | Text("Permissions")
27 | }
28 | .font(.title)
29 | .bold()
30 | .frame(height: 200)
31 |
32 | Form {
33 | PermissionsSection()
34 | }
35 | .formStyle(.grouped)
36 | .scrollDisabled(true)
37 | }
38 | .padding()
39 | .frame(width: BeginningViewController.size.width)
40 | .frame(maxHeight: .infinity)
41 |
42 | #if !DEBUG
43 | // .onChange(of: PermissionsManager.remaining, initial: true) { old, new in
44 | // canContinue(new == 0)
45 | // }
46 | #endif
47 | }
48 | }
49 |
50 | #Preview {
51 | BeginningPermissionsPage()
52 | }
53 |
--------------------------------------------------------------------------------
/Clipchop/Views/Beginning/Pages/BeginningShortcutsPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BeginningShortcutsPage.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BeginningShortcutsPage: View {
11 | @Environment(\.namespace) private var namespace
12 | @Environment(\.isVisible) private var isVisible
13 |
14 | var body: some View {
15 | VStack {
16 | VStack {
17 | if isVisible {
18 | Image(systemSymbol: .keyboard)
19 | .imageScale(.large)
20 | .padding()
21 | .matchedGeometryEffect(id: "flip", in: namespace!)
22 | .transition(.rotate3D(angle: .degrees(65)).combined(with: .scale).combined(with: .opacity))
23 | }
24 |
25 | Text("Shortcuts")
26 | }
27 | .font(.title)
28 | .bold()
29 | .frame(height: 200)
30 |
31 | Form {
32 | KeyboardShortcutsSection()
33 | .controlSize(.large)
34 | }
35 | .formStyle(.grouped)
36 | .scrollDisabled(true)
37 | }
38 | .padding()
39 | .frame(width: BeginningViewController.size.width)
40 | .frame(maxHeight: .infinity)
41 | }
42 | }
43 |
44 | #Preview {
45 | BeginningShortcutsPage()
46 | }
47 |
--------------------------------------------------------------------------------
/Clipchop/Views/Beginning/Pages/BeginningTutorialPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BeginningTutorialPage.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BeginningTutorialPage: View {
11 | @Environment(\.namespace) private var namespace
12 | @Environment(\.isVisible) private var isVisible
13 |
14 | var body: some View {
15 | Group {
16 | VStack {
17 | if isVisible {
18 | Image(systemSymbol: .lightbulb)
19 | .imageScale(.large)
20 | .padding()
21 | .matchedGeometryEffect(id: "flip", in: namespace!)
22 | .transition(.rotate3D(angle: .degrees(65)).combined(with: .scale).combined(with: .opacity))
23 | }
24 |
25 | Text("Tutorial")
26 | }
27 | .font(.title)
28 | .bold()
29 | .frame(maxHeight: .infinity)
30 | }
31 | .frame(width: BeginningViewController.size.width)
32 | .frame(maxHeight: .infinity)
33 | }
34 | }
35 |
36 | #Preview {
37 | BeginningTutorialPage()
38 | }
39 |
--------------------------------------------------------------------------------
/Clipchop/Views/Beginning/RoamingButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoamingButton.swift
3 | // Clipchop
4 | //
5 | // Created by KrLite on 2024/5/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RoamingButton