├── ChatGPT.dmg
├── resource
└── snapshot.png
├── ChatGPT-Mac-MenuBar
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── 16.png
│ │ ├── 32.png
│ │ ├── 64.png
│ │ ├── 1024.png
│ │ ├── 128.png
│ │ ├── 256.png
│ │ ├── 32 1.png
│ │ ├── 512.png
│ │ ├── 1024 1.png
│ │ ├── 256 1.png
│ │ ├── 512 1.png
│ │ └── Contents.json
│ ├── menu_bar_icon.imageset
│ │ ├── menu_bar_icon.png
│ │ ├── menu_bar_icon@2x.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── Info.plist
├── ChatGPT_Mac_MenuBar.entitlements
├── main.swift
├── MainNSViewController.swift
├── MainUI.swift
├── AppDelegate.swift
└── SwiftUIWebView.swift
├── .gitignore
├── README.md
├── LICENSE
└── ChatGPT-Mac-MenuBar.xcodeproj
├── xcshareddata
└── xcschemes
│ └── ChatGPT-Mac-MenuBar.xcscheme
└── project.pbxproj
/ChatGPT.dmg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT.dmg
--------------------------------------------------------------------------------
/resource/snapshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/resource/snapshot.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/16.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/32.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/64.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/128.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/256.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/32 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/32 1.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/512.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/1024 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/1024 1.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/256 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/256 1.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/512 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/512 1.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/menu_bar_icon.imageset/menu_bar_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/menu_bar_icon.imageset/menu_bar_icon.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/menu_bar_icon.imageset/menu_bar_icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KittenYang/ChatGPT-Mac-MenuBar/HEAD/ChatGPT-Mac-MenuBar/Assets.xcassets/menu_bar_icon.imageset/menu_bar_icon@2x.png
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/ChatGPT_Mac_MenuBar.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 | com.apple.security.network.client
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/menu_bar_icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "menu_bar_icon.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "menu_bar_icon@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ruby on Rails
2 | *.tmproj
3 | tmp/**/*
4 | config/database.yml
5 | config/*.sphinx.conf
6 | db/sphinx/**/*
7 | db/schema.rb
8 | log/*
9 | .rvmrc
10 |
11 | # Mac OS X Finder and whatnot
12 | .DS_Store
13 |
14 | # XCode (and ancestors) per-user config (very noisy, and not relevant)
15 | *.mode1
16 | *.mode1v3
17 | *.mode2v3
18 | *.perspective
19 | *.perspectivev3
20 | *.pbxuser
21 | *.xcworkspace
22 | xcuserdata/
23 |
24 | # Generated files
25 | VersionX-revision.h
26 |
27 | # build products
28 | build/
29 | *.[oa]
30 |
31 | # Other source repository archive directories (protects when importing)
32 | .hg
33 | .svn/*/**
34 | CVS
35 |
36 | # automatic backup files
37 | *~.nib
38 | *.swp
39 | *~
40 | *(Autosaved).rtfd/
41 | Backup[ ]of[ ]*.pages/
42 | Backup[ ]of[ ]*.key/
43 | Backup[ ]of[ ]*.numbers/
44 |
45 | Pods/
46 | Podfile.lock
47 | /PodMainDependencies/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ChatGPT-Mac-MenuBar
2 | Chat with OpenAI's ChatGPT in mac menu bar like a pro.
3 |
4 | ✅ Lightweight. Only 650KB.
5 |
6 | ✅ `shift+cmd+C` to trigger popover globally
7 |
8 | ✅ **Right click** on the menu bar icon to **Clean Cookies** for login another OpenAI's account
9 |
10 | ✅ Handle keyboard shortcuts `cmd+c/v/x/z/a` with webview
11 |
12 | ✅ Popover Window Resizable
13 |
14 |
15 | 
16 |
17 | # Requirements
18 | macOS 12.5+
19 |
20 |
21 | # Credit
22 | [OpenAI](https://openai.com/)
23 |
24 | # Download
25 | Instead of building the source code, you can use the archived .dmg file right away!
26 |
27 | [ChatGPT.dmg](https://github.com/KittenYang/ChatGPT-Mac-MenuBar/raw/main/ChatGPT.dmg)
28 |
29 |
30 | # Note
31 | It is a very basic app to bridge a webview to the status bar. So it must have inevitable limits. Feel free to make pull requests or reach me on [@KittenYang](https://twitter.com/KittenYang)
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Qitao Yang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // ChatGPT-Mac-MenuBar
4 | //
5 | // Created by Qitao Yang on 12/5/22.
6 | //
7 | // Copyright (c) 2022 KittenYang
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 |
28 | import Foundation
29 | import AppKit
30 |
31 |
32 | let app = NSApplication.shared
33 | let delegate = AppDelegate()
34 | app.delegate = delegate
35 |
36 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
37 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "1024 1.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | },
9 | {
10 | "filename" : "16.png",
11 | "idiom" : "mac",
12 | "scale" : "1x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "32 1.png",
17 | "idiom" : "mac",
18 | "scale" : "2x",
19 | "size" : "16x16"
20 | },
21 | {
22 | "filename" : "32.png",
23 | "idiom" : "mac",
24 | "scale" : "1x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "64.png",
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "32x32"
32 | },
33 | {
34 | "filename" : "128.png",
35 | "idiom" : "mac",
36 | "scale" : "1x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "256 1.png",
41 | "idiom" : "mac",
42 | "scale" : "2x",
43 | "size" : "128x128"
44 | },
45 | {
46 | "filename" : "256.png",
47 | "idiom" : "mac",
48 | "scale" : "1x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "512 1.png",
53 | "idiom" : "mac",
54 | "scale" : "2x",
55 | "size" : "256x256"
56 | },
57 | {
58 | "filename" : "512.png",
59 | "idiom" : "mac",
60 | "scale" : "1x",
61 | "size" : "512x512"
62 | },
63 | {
64 | "filename" : "1024.png",
65 | "idiom" : "mac",
66 | "scale" : "2x",
67 | "size" : "512x512"
68 | }
69 | ],
70 | "info" : {
71 | "author" : "xcode",
72 | "version" : 1
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/MainNSViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainNSViewController.swift
3 | // ChatGPT-Mac-MenuBar
4 | //
5 | // Created by Qitao Yang on 12/5/22.
6 | //
7 | // Copyright (c) 2022 KittenYang
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 |
28 | import SwiftUI
29 | import AppKit
30 |
31 | class MainNSViewController: NSViewController {
32 | override func loadView() {
33 | // create a hosting controller with your SwiftUI view
34 | let hostingController = NSHostingController(rootView: MainUI())
35 | self.view = hostingController.view
36 | self.view.frame = CGRect(origin: .zero, size: .init(width: 500, height: 600))
37 | }
38 | override func mouseDragged(with event: NSEvent) {
39 | guard let appDelegate: AppDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
40 | var size = appDelegate.popover?.contentSize ?? CGSize.zero
41 | size.width += event.deltaX
42 | size.height += event.deltaY
43 | // Update popover size depend on your reference
44 | appDelegate.popover?.contentSize = size
45 | }
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar.xcodeproj/xcshareddata/xcschemes/ChatGPT-Mac-MenuBar.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/MainUI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainUI.swift
3 | // ChatGPT-Mac-MenuBar
4 | //
5 | // Created by Qitao Yang on 12/5/22.
6 | //
7 | // Copyright (c) 2022 KittenYang
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 |
28 | import SwiftUI
29 | import WebKit
30 |
31 | struct MainUI: View {
32 | @State private var action = WebViewAction.idle
33 | @State private var state = WebViewState.empty
34 | @State private var address = "https://chat.openai.com/chat"
35 |
36 | var webConfig: WebViewConfig {
37 | var defaultC = WebViewConfig.default
38 | defaultC.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1"
39 | // defaultC.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
40 | return defaultC
41 | }
42 |
43 | var body: some View {
44 | VStack(spacing:0.0) {
45 | navigationToolbar
46 | errorView
47 | WebView(config: webConfig,
48 | action: $action,
49 | state: $state,
50 | restrictedPages: nil)
51 | Image(systemName: "line.3.horizontal")
52 | }
53 | .onAppear {
54 | if let url = URL(string: address) {
55 | action = .load(URLRequest(url: url))
56 | }
57 | }
58 | .background(Color.init(nsColor: .windowBackgroundColor))
59 | }
60 |
61 | private var navigationToolbar: some View {
62 | HStack(spacing: 10) {
63 | if state.isLoading {
64 | if #available(iOS 14, macOS 10.15, *) {
65 | ProgressView()
66 | .progressViewStyle(CircularProgressViewStyle())
67 | } else {
68 | Text("Loading")
69 | }
70 | }
71 | Spacer()
72 | Button(action: {
73 | action = .reload
74 | }) {
75 | Image(systemName: "arrow.counterclockwise")
76 | .imageScale(.large)
77 | .foregroundColor(.init(nsColor: .labelColor))
78 | }
79 | if state.canGoBack {
80 | Button(action: {
81 | action = .goBack
82 | }) {
83 | Image(systemName: "chevron.left")
84 | .imageScale(.large)
85 | .foregroundColor(.init(nsColor: .labelColor))
86 | }
87 | }
88 | if state.canGoForward {
89 | Button(action: {
90 | action = .goForward
91 | }) {
92 | Image(systemName: "chevron.right")
93 | .imageScale(.large)
94 | .foregroundColor(.init(nsColor: .labelColor))
95 |
96 | }
97 | }
98 | }.background(Color.init(nsColor: .windowBackgroundColor))
99 | .padding([.top,.leading,.trailing,.bottom],15.0)
100 | }
101 |
102 | private var errorView: some View {
103 | Group {
104 | if let error = state.error {
105 | Text(error.localizedDescription)
106 | .foregroundColor(.red)
107 | }
108 | }
109 | }
110 | }
111 |
112 | struct WebViewHelper {
113 | static func clean() {
114 | HTTPCookieStorage.shared.removeCookies(since: Date.distantPast)
115 | print("[WebCacheCleaner] All cookies deleted")
116 |
117 | WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
118 | records.forEach { record in
119 | WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})
120 | print("[WebCacheCleaner] Record \(record) deleted")
121 | }
122 | }
123 | }
124 | }
125 |
126 | struct MainUI_Previews: PreviewProvider {
127 | static var previews: some View {
128 | MainUI()
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // ChatGPT-Mac-MenuBar
4 | //
5 | // Created by Qitao Yang on 12/5/22.
6 | //
7 | // Copyright (c) 2022 KittenYang
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 |
28 | import Cocoa
29 | import SwiftUI
30 | import HotKey
31 |
32 | class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
33 |
34 | private var statusItem: NSStatusItem!
35 |
36 | public var popover: NSPopover!
37 | private var menu: NSMenu!
38 |
39 | let hotKey = HotKey(key: .c, modifiers: [.shift, .command]) // Global hotkey
40 |
41 | var hotCKey: HotKey?
42 | var hotVKey: HotKey?
43 | var hotZKey: HotKey?
44 | var hotXKey: HotKey?
45 | var hotAKey: HotKey?
46 |
47 | func applicationDidFinishLaunching(_ aNotification: Notification) {
48 |
49 | statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
50 |
51 | if let button = statusItem.button {
52 | let icon = NSImage(named: "menu_bar_icon")
53 | icon?.isTemplate = true
54 | button.image = icon
55 | button.action = #selector(self.handleMenuIconAction(sender:))
56 | button.sendAction(on: [.leftMouseUp, .rightMouseUp])
57 | }
58 |
59 | constructPopover()
60 | constructMenu()
61 |
62 | hotKey.keyUpHandler = { // Global hotkey handler
63 | self.togglePopover()
64 | }
65 |
66 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
67 | self.togglePopover()
68 | }
69 | }
70 |
71 | @objc func handleMenuIconAction(sender: NSStatusBarButton) {
72 | let event = NSApp.currentEvent!
73 | if event.type == NSEvent.EventType.rightMouseUp {
74 | showMenu()
75 | } else {
76 | removeMenu()
77 | togglePopover()
78 | }
79 | }
80 |
81 | func menuDidClose(_ menu: NSMenu) {
82 | removeMenu()
83 | }
84 |
85 | @objc func didTapOne() {
86 | WebViewHelper.clean()
87 | }
88 |
89 | func constructMenu() {
90 | menu = NSMenu()
91 | let one = NSMenuItem(title: "Clean Cookies", action: #selector(didTapOne) , keyEquivalent: "1")
92 | menu.addItem(one)
93 | menu.addItem(NSMenuItem.separator())
94 | menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
95 | menu.delegate = self
96 | }
97 |
98 | func constructPopover() {
99 | popover = NSPopover()
100 | popover.contentViewController = MainNSViewController()
101 | popover.delegate = self
102 | popover.behavior = NSPopover.Behavior.transient;
103 | }
104 |
105 | func showMenu() {
106 | statusItem.menu = menu
107 | statusItem.popUpMenu(menu)
108 | }
109 |
110 | func removeMenu() {
111 | statusItem.menu = nil
112 | }
113 |
114 | func togglePopover() {
115 | if popover.isShown {
116 | popover.performClose(nil)
117 | deinitKeys()
118 | } else {
119 | if let button = statusItem.button {
120 | NSApplication.shared.activate(ignoringOtherApps: true)
121 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
122 | popover.contentViewController?.view.window?.level = .floating
123 | popover.contentViewController?.view.window?.makeKey()
124 | constructKeys()
125 | }
126 | }
127 | }
128 |
129 | private func deinitKeys() {
130 | hotCKey = nil
131 | hotVKey = nil
132 | hotXKey = nil
133 | hotZKey = nil
134 | hotAKey = nil
135 | }
136 |
137 | private func constructKeys() {
138 |
139 | hotCKey = HotKey(key: .c, modifiers: [.command]) // Global hotkey
140 | hotVKey = HotKey(key: .v, modifiers: [.command]) // Global hotkey
141 | hotZKey = HotKey(key: .z, modifiers: [.command]) // Global hotkey
142 | hotXKey = HotKey(key: .x, modifiers: [.command]) // Global hotkey
143 | hotAKey = HotKey(key: .a, modifiers: [.command]) // Global hotkey
144 |
145 | hotCKey?.keyDownHandler = {
146 | NSApp.sendAction(#selector(NSText.copy(_:)), to:nil, from:self)
147 | }
148 |
149 | hotVKey?.keyDownHandler = {
150 | NSApp.sendAction(#selector(NSText.paste(_:)), to:nil, from:self)
151 | }
152 |
153 | hotXKey?.keyDownHandler = {
154 | NSApp.sendAction(#selector(NSText.cut(_:)), to:nil, from:self)
155 | }
156 |
157 | hotZKey?.keyDownHandler = {
158 | NSApp.sendAction(Selector("undo:"), to:nil, from:self)
159 | }
160 |
161 | hotAKey?.keyDownHandler = {
162 | NSApp.sendAction(#selector(NSStandardKeyBindingResponding.selectAll(_:)), to:nil, from:self)
163 | }
164 | }
165 |
166 | func applicationWillTerminate(_ aNotification: Notification) {
167 | // Insert code here to tear down your application
168 | }
169 |
170 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
171 | return true
172 | }
173 |
174 |
175 | }
176 |
177 | extension AppDelegate: NSPopoverDelegate {
178 | func popoverWillClose(_ notification: Notification) {
179 | self.deinitKeys()
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 336A9F57293DCEEE0041AF09 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336A9F56293DCEEE0041AF09 /* AppDelegate.swift */; };
11 | 336A9F59293DCEEE0041AF09 /* MainNSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336A9F58293DCEEE0041AF09 /* MainNSViewController.swift */; };
12 | 336A9F5B293DCEEF0041AF09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 336A9F5A293DCEEF0041AF09 /* Assets.xcassets */; };
13 | 336A9F67293DD0820041AF09 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336A9F66293DD0820041AF09 /* main.swift */; };
14 | 336A9F69293DD1150041AF09 /* MainUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336A9F68293DD1150041AF09 /* MainUI.swift */; };
15 | 336A9F71293E0C410041AF09 /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = 336A9F70293E0C410041AF09 /* HotKey */; };
16 | 3396A0F1293E350300A1F26A /* SwiftUIWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3396A0F0293E350300A1F26A /* SwiftUIWebView.swift */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXFileReference section */
20 | 336A9F53293DCEEE0041AF09 /* ChatGPT.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatGPT.app; sourceTree = BUILT_PRODUCTS_DIR; };
21 | 336A9F56293DCEEE0041AF09 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
22 | 336A9F58293DCEEE0041AF09 /* MainNSViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNSViewController.swift; sourceTree = ""; };
23 | 336A9F5A293DCEEF0041AF09 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
24 | 336A9F5F293DCEEF0041AF09 /* ChatGPT_Mac_MenuBar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ChatGPT_Mac_MenuBar.entitlements; sourceTree = ""; };
25 | 336A9F66293DD0820041AF09 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
26 | 336A9F68293DD1150041AF09 /* MainUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUI.swift; sourceTree = ""; };
27 | 336A9F6E293E0AE90041AF09 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
28 | 3396A0F0293E350300A1F26A /* SwiftUIWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIWebView.swift; sourceTree = ""; };
29 | /* End PBXFileReference section */
30 |
31 | /* Begin PBXFrameworksBuildPhase section */
32 | 336A9F50293DCEEE0041AF09 /* Frameworks */ = {
33 | isa = PBXFrameworksBuildPhase;
34 | buildActionMask = 2147483647;
35 | files = (
36 | 336A9F71293E0C410041AF09 /* HotKey in Frameworks */,
37 | );
38 | runOnlyForDeploymentPostprocessing = 0;
39 | };
40 | /* End PBXFrameworksBuildPhase section */
41 |
42 | /* Begin PBXGroup section */
43 | 336A9F4A293DCEEE0041AF09 = {
44 | isa = PBXGroup;
45 | children = (
46 | 336A9F55293DCEEE0041AF09 /* ChatGPT-Mac-MenuBar */,
47 | 336A9F54293DCEEE0041AF09 /* Products */,
48 | );
49 | sourceTree = "";
50 | };
51 | 336A9F54293DCEEE0041AF09 /* Products */ = {
52 | isa = PBXGroup;
53 | children = (
54 | 336A9F53293DCEEE0041AF09 /* ChatGPT.app */,
55 | );
56 | name = Products;
57 | sourceTree = "";
58 | };
59 | 336A9F55293DCEEE0041AF09 /* ChatGPT-Mac-MenuBar */ = {
60 | isa = PBXGroup;
61 | children = (
62 | 336A9F6E293E0AE90041AF09 /* Info.plist */,
63 | 336A9F56293DCEEE0041AF09 /* AppDelegate.swift */,
64 | 336A9F66293DD0820041AF09 /* main.swift */,
65 | 336A9F58293DCEEE0041AF09 /* MainNSViewController.swift */,
66 | 336A9F68293DD1150041AF09 /* MainUI.swift */,
67 | 3396A0F0293E350300A1F26A /* SwiftUIWebView.swift */,
68 | 336A9F5A293DCEEF0041AF09 /* Assets.xcassets */,
69 | 336A9F5F293DCEEF0041AF09 /* ChatGPT_Mac_MenuBar.entitlements */,
70 | );
71 | path = "ChatGPT-Mac-MenuBar";
72 | sourceTree = "";
73 | };
74 | /* End PBXGroup section */
75 |
76 | /* Begin PBXNativeTarget section */
77 | 336A9F52293DCEEE0041AF09 /* ChatGPT-Mac-MenuBar */ = {
78 | isa = PBXNativeTarget;
79 | buildConfigurationList = 336A9F62293DCEEF0041AF09 /* Build configuration list for PBXNativeTarget "ChatGPT-Mac-MenuBar" */;
80 | buildPhases = (
81 | 336A9F4F293DCEEE0041AF09 /* Sources */,
82 | 336A9F50293DCEEE0041AF09 /* Frameworks */,
83 | 336A9F51293DCEEE0041AF09 /* Resources */,
84 | );
85 | buildRules = (
86 | );
87 | dependencies = (
88 | );
89 | name = "ChatGPT-Mac-MenuBar";
90 | packageProductDependencies = (
91 | 336A9F70293E0C410041AF09 /* HotKey */,
92 | );
93 | productName = "ChatGPT-Mac-MenuBar";
94 | productReference = 336A9F53293DCEEE0041AF09 /* ChatGPT.app */;
95 | productType = "com.apple.product-type.application";
96 | };
97 | /* End PBXNativeTarget section */
98 |
99 | /* Begin PBXProject section */
100 | 336A9F4B293DCEEE0041AF09 /* Project object */ = {
101 | isa = PBXProject;
102 | attributes = {
103 | BuildIndependentTargetsInParallel = 1;
104 | LastSwiftUpdateCheck = 1410;
105 | LastUpgradeCheck = 1410;
106 | TargetAttributes = {
107 | 336A9F52293DCEEE0041AF09 = {
108 | CreatedOnToolsVersion = 14.1;
109 | };
110 | };
111 | };
112 | buildConfigurationList = 336A9F4E293DCEEE0041AF09 /* Build configuration list for PBXProject "ChatGPT-Mac-MenuBar" */;
113 | compatibilityVersion = "Xcode 14.0";
114 | developmentRegion = en;
115 | hasScannedForEncodings = 0;
116 | knownRegions = (
117 | en,
118 | Base,
119 | );
120 | mainGroup = 336A9F4A293DCEEE0041AF09;
121 | packageReferences = (
122 | 336A9F6F293E0C410041AF09 /* XCRemoteSwiftPackageReference "HotKey" */,
123 | );
124 | productRefGroup = 336A9F54293DCEEE0041AF09 /* Products */;
125 | projectDirPath = "";
126 | projectRoot = "";
127 | targets = (
128 | 336A9F52293DCEEE0041AF09 /* ChatGPT-Mac-MenuBar */,
129 | );
130 | };
131 | /* End PBXProject section */
132 |
133 | /* Begin PBXResourcesBuildPhase section */
134 | 336A9F51293DCEEE0041AF09 /* Resources */ = {
135 | isa = PBXResourcesBuildPhase;
136 | buildActionMask = 2147483647;
137 | files = (
138 | 336A9F5B293DCEEF0041AF09 /* Assets.xcassets in Resources */,
139 | );
140 | runOnlyForDeploymentPostprocessing = 0;
141 | };
142 | /* End PBXResourcesBuildPhase section */
143 |
144 | /* Begin PBXSourcesBuildPhase section */
145 | 336A9F4F293DCEEE0041AF09 /* Sources */ = {
146 | isa = PBXSourcesBuildPhase;
147 | buildActionMask = 2147483647;
148 | files = (
149 | 336A9F59293DCEEE0041AF09 /* MainNSViewController.swift in Sources */,
150 | 336A9F57293DCEEE0041AF09 /* AppDelegate.swift in Sources */,
151 | 336A9F69293DD1150041AF09 /* MainUI.swift in Sources */,
152 | 3396A0F1293E350300A1F26A /* SwiftUIWebView.swift in Sources */,
153 | 336A9F67293DD0820041AF09 /* main.swift in Sources */,
154 | );
155 | runOnlyForDeploymentPostprocessing = 0;
156 | };
157 | /* End PBXSourcesBuildPhase section */
158 |
159 | /* Begin XCBuildConfiguration section */
160 | 336A9F60293DCEEF0041AF09 /* Debug */ = {
161 | isa = XCBuildConfiguration;
162 | buildSettings = {
163 | ALWAYS_SEARCH_USER_PATHS = NO;
164 | CLANG_ANALYZER_NONNULL = YES;
165 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
166 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
167 | CLANG_ENABLE_MODULES = YES;
168 | CLANG_ENABLE_OBJC_ARC = YES;
169 | CLANG_ENABLE_OBJC_WEAK = YES;
170 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
171 | CLANG_WARN_BOOL_CONVERSION = YES;
172 | CLANG_WARN_COMMA = YES;
173 | CLANG_WARN_CONSTANT_CONVERSION = YES;
174 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
175 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
176 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
177 | CLANG_WARN_EMPTY_BODY = YES;
178 | CLANG_WARN_ENUM_CONVERSION = YES;
179 | CLANG_WARN_INFINITE_RECURSION = YES;
180 | CLANG_WARN_INT_CONVERSION = YES;
181 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
182 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
183 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
184 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
185 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
186 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
187 | CLANG_WARN_STRICT_PROTOTYPES = YES;
188 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
189 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
190 | CLANG_WARN_UNREACHABLE_CODE = YES;
191 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
192 | COPY_PHASE_STRIP = NO;
193 | DEBUG_INFORMATION_FORMAT = dwarf;
194 | ENABLE_STRICT_OBJC_MSGSEND = YES;
195 | ENABLE_TESTABILITY = YES;
196 | GCC_C_LANGUAGE_STANDARD = gnu11;
197 | GCC_DYNAMIC_NO_PIC = NO;
198 | GCC_NO_COMMON_BLOCKS = YES;
199 | GCC_OPTIMIZATION_LEVEL = 0;
200 | GCC_PREPROCESSOR_DEFINITIONS = (
201 | "DEBUG=1",
202 | "$(inherited)",
203 | );
204 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
205 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
206 | GCC_WARN_UNDECLARED_SELECTOR = YES;
207 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
208 | GCC_WARN_UNUSED_FUNCTION = YES;
209 | GCC_WARN_UNUSED_VARIABLE = YES;
210 | MACOSX_DEPLOYMENT_TARGET = 12.5;
211 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
212 | MTL_FAST_MATH = YES;
213 | ONLY_ACTIVE_ARCH = YES;
214 | SDKROOT = macosx;
215 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
216 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
217 | };
218 | name = Debug;
219 | };
220 | 336A9F61293DCEEF0041AF09 /* Release */ = {
221 | isa = XCBuildConfiguration;
222 | buildSettings = {
223 | ALWAYS_SEARCH_USER_PATHS = NO;
224 | CLANG_ANALYZER_NONNULL = YES;
225 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
226 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
227 | CLANG_ENABLE_MODULES = YES;
228 | CLANG_ENABLE_OBJC_ARC = YES;
229 | CLANG_ENABLE_OBJC_WEAK = YES;
230 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
231 | CLANG_WARN_BOOL_CONVERSION = YES;
232 | CLANG_WARN_COMMA = YES;
233 | CLANG_WARN_CONSTANT_CONVERSION = YES;
234 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
235 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
236 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
237 | CLANG_WARN_EMPTY_BODY = YES;
238 | CLANG_WARN_ENUM_CONVERSION = YES;
239 | CLANG_WARN_INFINITE_RECURSION = YES;
240 | CLANG_WARN_INT_CONVERSION = YES;
241 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
242 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
243 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
244 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
245 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
246 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
247 | CLANG_WARN_STRICT_PROTOTYPES = YES;
248 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
249 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
250 | CLANG_WARN_UNREACHABLE_CODE = YES;
251 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
252 | COPY_PHASE_STRIP = NO;
253 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
254 | ENABLE_NS_ASSERTIONS = NO;
255 | ENABLE_STRICT_OBJC_MSGSEND = YES;
256 | GCC_C_LANGUAGE_STANDARD = gnu11;
257 | GCC_NO_COMMON_BLOCKS = YES;
258 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
259 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
260 | GCC_WARN_UNDECLARED_SELECTOR = YES;
261 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
262 | GCC_WARN_UNUSED_FUNCTION = YES;
263 | GCC_WARN_UNUSED_VARIABLE = YES;
264 | MACOSX_DEPLOYMENT_TARGET = 12.5;
265 | MTL_ENABLE_DEBUG_INFO = NO;
266 | MTL_FAST_MATH = YES;
267 | SDKROOT = macosx;
268 | SWIFT_COMPILATION_MODE = wholemodule;
269 | SWIFT_OPTIMIZATION_LEVEL = "-O";
270 | };
271 | name = Release;
272 | };
273 | 336A9F63293DCEEF0041AF09 /* Debug */ = {
274 | isa = XCBuildConfiguration;
275 | buildSettings = {
276 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
277 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
278 | CODE_SIGN_ENTITLEMENTS = "ChatGPT-Mac-MenuBar/ChatGPT_Mac_MenuBar.entitlements";
279 | CODE_SIGN_STYLE = Automatic;
280 | COMBINE_HIDPI_IMAGES = YES;
281 | CURRENT_PROJECT_VERSION = 1;
282 | DEVELOPMENT_TEAM = R78WMUBTG5;
283 | ENABLE_HARDENED_RUNTIME = YES;
284 | GENERATE_INFOPLIST_FILE = YES;
285 | INFOPLIST_FILE = "ChatGPT-Mac-MenuBar/Info.plist";
286 | INFOPLIST_KEY_LSUIElement = YES;
287 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
288 | INFOPLIST_KEY_NSMainStoryboardFile = "";
289 | INFOPLIST_KEY_NSPrincipalClass = NSApplication;
290 | LD_RUNPATH_SEARCH_PATHS = (
291 | "$(inherited)",
292 | "@executable_path/../Frameworks",
293 | );
294 | MARKETING_VERSION = 1.0;
295 | PRODUCT_BUNDLE_IDENTIFIER = "com.kittenyang.ios.ChatGPT-Mac-MenuBar";
296 | PRODUCT_NAME = ChatGPT;
297 | SWIFT_EMIT_LOC_STRINGS = YES;
298 | SWIFT_VERSION = 5.0;
299 | };
300 | name = Debug;
301 | };
302 | 336A9F64293DCEEF0041AF09 /* Release */ = {
303 | isa = XCBuildConfiguration;
304 | buildSettings = {
305 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
306 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
307 | CODE_SIGN_ENTITLEMENTS = "ChatGPT-Mac-MenuBar/ChatGPT_Mac_MenuBar.entitlements";
308 | CODE_SIGN_STYLE = Automatic;
309 | COMBINE_HIDPI_IMAGES = YES;
310 | CURRENT_PROJECT_VERSION = 1;
311 | DEVELOPMENT_TEAM = R78WMUBTG5;
312 | ENABLE_HARDENED_RUNTIME = YES;
313 | GENERATE_INFOPLIST_FILE = YES;
314 | INFOPLIST_FILE = "ChatGPT-Mac-MenuBar/Info.plist";
315 | INFOPLIST_KEY_LSUIElement = YES;
316 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
317 | INFOPLIST_KEY_NSMainStoryboardFile = "";
318 | INFOPLIST_KEY_NSPrincipalClass = NSApplication;
319 | LD_RUNPATH_SEARCH_PATHS = (
320 | "$(inherited)",
321 | "@executable_path/../Frameworks",
322 | );
323 | MARKETING_VERSION = 1.0;
324 | PRODUCT_BUNDLE_IDENTIFIER = "com.kittenyang.ios.ChatGPT-Mac-MenuBar";
325 | PRODUCT_NAME = ChatGPT;
326 | SWIFT_EMIT_LOC_STRINGS = YES;
327 | SWIFT_VERSION = 5.0;
328 | };
329 | name = Release;
330 | };
331 | /* End XCBuildConfiguration section */
332 |
333 | /* Begin XCConfigurationList section */
334 | 336A9F4E293DCEEE0041AF09 /* Build configuration list for PBXProject "ChatGPT-Mac-MenuBar" */ = {
335 | isa = XCConfigurationList;
336 | buildConfigurations = (
337 | 336A9F60293DCEEF0041AF09 /* Debug */,
338 | 336A9F61293DCEEF0041AF09 /* Release */,
339 | );
340 | defaultConfigurationIsVisible = 0;
341 | defaultConfigurationName = Release;
342 | };
343 | 336A9F62293DCEEF0041AF09 /* Build configuration list for PBXNativeTarget "ChatGPT-Mac-MenuBar" */ = {
344 | isa = XCConfigurationList;
345 | buildConfigurations = (
346 | 336A9F63293DCEEF0041AF09 /* Debug */,
347 | 336A9F64293DCEEF0041AF09 /* Release */,
348 | );
349 | defaultConfigurationIsVisible = 0;
350 | defaultConfigurationName = Release;
351 | };
352 | /* End XCConfigurationList section */
353 |
354 | /* Begin XCRemoteSwiftPackageReference section */
355 | 336A9F6F293E0C410041AF09 /* XCRemoteSwiftPackageReference "HotKey" */ = {
356 | isa = XCRemoteSwiftPackageReference;
357 | repositoryURL = "https://github.com/soffes/HotKey.git";
358 | requirement = {
359 | branch = master;
360 | kind = branch;
361 | };
362 | };
363 | /* End XCRemoteSwiftPackageReference section */
364 |
365 | /* Begin XCSwiftPackageProductDependency section */
366 | 336A9F70293E0C410041AF09 /* HotKey */ = {
367 | isa = XCSwiftPackageProductDependency;
368 | package = 336A9F6F293E0C410041AF09 /* XCRemoteSwiftPackageReference "HotKey" */;
369 | productName = HotKey;
370 | };
371 | /* End XCSwiftPackageProductDependency section */
372 | };
373 | rootObject = 336A9F4B293DCEEE0041AF09 /* Project object */;
374 | }
375 |
--------------------------------------------------------------------------------
/ChatGPT-Mac-MenuBar/SwiftUIWebView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import WebKit
3 | import Foundation
4 |
5 | public enum WebViewAction: Equatable {
6 | case idle,
7 | load(URLRequest),
8 | loadHTML(String),
9 | reload,
10 | goBack,
11 | goForward,
12 | evaluateJS(String, (Result) -> Void)
13 |
14 |
15 | public static func == (lhs: WebViewAction, rhs: WebViewAction) -> Bool {
16 | if case .idle = lhs,
17 | case .idle = rhs {
18 | return true
19 | }
20 | if case let .load(requestLHS) = lhs,
21 | case let .load(requestRHS) = rhs {
22 | return requestLHS == requestRHS
23 | }
24 | if case let .loadHTML(htmlLHS) = lhs,
25 | case let .loadHTML(htmlRHS) = rhs {
26 | return htmlLHS == htmlRHS
27 | }
28 | if case .reload = lhs,
29 | case .reload = rhs {
30 | return true
31 | }
32 | if case .goBack = lhs,
33 | case .goBack = rhs {
34 | return true
35 | }
36 | if case .goForward = lhs,
37 | case .goForward = rhs {
38 | return true
39 | }
40 | if case let .evaluateJS(commandLHS, _) = lhs,
41 | case let .evaluateJS(commandRHS, _) = rhs {
42 | return commandLHS == commandRHS
43 | }
44 | return false
45 | }
46 | }
47 |
48 | public struct WebViewState: Equatable {
49 | public internal(set) var isLoading: Bool
50 | public internal(set) var pageURL: String?
51 | public internal(set) var pageTitle: String?
52 | public internal(set) var pageHTML: String?
53 | public internal(set) var error: Error?
54 | public internal(set) var canGoBack: Bool
55 | public internal(set) var canGoForward: Bool
56 |
57 | public static let empty = WebViewState(isLoading: false,
58 | pageURL: nil,
59 | pageTitle: nil,
60 | pageHTML: nil,
61 | error: nil,
62 | canGoBack: false,
63 | canGoForward: false)
64 |
65 | public static func == (lhs: WebViewState, rhs: WebViewState) -> Bool {
66 | lhs.isLoading == rhs.isLoading
67 | && lhs.pageURL == rhs.pageURL
68 | && lhs.pageTitle == rhs.pageTitle
69 | && lhs.pageHTML == rhs.pageHTML
70 | && lhs.error?.localizedDescription == rhs.error?.localizedDescription
71 | && lhs.canGoBack == rhs.canGoBack
72 | && lhs.canGoForward == rhs.canGoForward
73 | }
74 | }
75 |
76 | public class WebViewCoordinator: NSObject {
77 | private let webView: WebView
78 | var actionInProgress = false
79 |
80 | init(webView: WebView) {
81 | self.webView = webView
82 | }
83 |
84 | func setLoading(_ isLoading: Bool,
85 | canGoBack: Bool? = nil,
86 | canGoForward: Bool? = nil,
87 | error: Error? = nil) {
88 | var newState = webView.state
89 | newState.isLoading = isLoading
90 | if let canGoBack = canGoBack {
91 | newState.canGoBack = canGoBack
92 | }
93 | if let canGoForward = canGoForward {
94 | newState.canGoForward = canGoForward
95 | }
96 | if let error = error {
97 | newState.error = error
98 | }
99 | webView.state = newState
100 | webView.action = .idle
101 | actionInProgress = false
102 | }
103 | }
104 |
105 | extension WebViewCoordinator: WKNavigationDelegate {
106 | public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
107 | setLoading(false,
108 | canGoBack: webView.canGoBack,
109 | canGoForward: webView.canGoForward)
110 |
111 | webView.evaluateJavaScript("document.title") { (response, error) in
112 | if let title = response as? String {
113 | var newState = self.webView.state
114 | newState.pageTitle = title
115 | self.webView.state = newState
116 | }
117 | }
118 |
119 | webView.evaluateJavaScript("document.URL.toString()") { (response, error) in
120 | if let url = response as? String {
121 | var newState = self.webView.state
122 | newState.pageURL = url
123 | self.webView.state = newState
124 | }
125 | }
126 |
127 | if self.webView.htmlInState {
128 | webView.evaluateJavaScript("document.documentElement.outerHTML.toString()") { (response, error) in
129 | if let html = response as? String {
130 | var newState = self.webView.state
131 | newState.pageHTML = html
132 | self.webView.state = newState
133 | }
134 | }
135 | }
136 | }
137 |
138 | public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
139 | setLoading(false)
140 | }
141 |
142 | public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
143 | setLoading(false, error: error)
144 | }
145 |
146 | public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
147 | setLoading(true)
148 | }
149 |
150 | public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
151 | setLoading(true,
152 | canGoBack: webView.canGoBack,
153 | canGoForward: webView.canGoForward)
154 | }
155 |
156 | public func webView(_ webView: WKWebView,
157 | decidePolicyFor navigationAction: WKNavigationAction,
158 | decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
159 | if let host = navigationAction.request.url?.host {
160 | if self.webView.restrictedPages?.first(where: { host.contains($0) }) != nil {
161 | decisionHandler(.cancel)
162 | setLoading(false)
163 | return
164 | }
165 | }
166 | if let url = navigationAction.request.url,
167 | let scheme = url.scheme,
168 | let schemeHandler = self.webView.schemeHandlers[scheme] {
169 | schemeHandler(url)
170 | decisionHandler(.cancel)
171 | return
172 | }
173 | decisionHandler(.allow)
174 | }
175 | }
176 |
177 | extension WebViewCoordinator: WKUIDelegate {
178 | public func webView(_ webView: WKWebView,
179 | createWebViewWith configuration: WKWebViewConfiguration,
180 | for navigationAction: WKNavigationAction,
181 | windowFeatures: WKWindowFeatures) -> WKWebView? {
182 | if navigationAction.targetFrame == nil {
183 | webView.load(navigationAction.request)
184 | }
185 | return nil
186 | }
187 | }
188 |
189 | public struct WebViewConfig {
190 | public static let `default` = WebViewConfig()
191 |
192 | public let javaScriptEnabled: Bool
193 | public let allowsBackForwardNavigationGestures: Bool
194 | public let allowsInlineMediaPlayback: Bool
195 | public let mediaTypesRequiringUserActionForPlayback: WKAudiovisualMediaTypes
196 | public let isScrollEnabled: Bool
197 | public let isOpaque: Bool
198 | public let backgroundColor: Color
199 | public var customUserAgent:String?
200 |
201 | public init(javaScriptEnabled: Bool = true,
202 | allowsBackForwardNavigationGestures: Bool = true,
203 | allowsInlineMediaPlayback: Bool = true,
204 | mediaTypesRequiringUserActionForPlayback: WKAudiovisualMediaTypes = [],
205 | isScrollEnabled: Bool = true,
206 | isOpaque: Bool = true,
207 | backgroundColor: Color = .clear,
208 | customUserAgent: String? = nil) {
209 | self.javaScriptEnabled = javaScriptEnabled
210 | self.allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures
211 | self.allowsInlineMediaPlayback = allowsInlineMediaPlayback
212 | self.mediaTypesRequiringUserActionForPlayback = mediaTypesRequiringUserActionForPlayback
213 | self.isScrollEnabled = isScrollEnabled
214 | self.isOpaque = isOpaque
215 | self.backgroundColor = backgroundColor
216 | self.customUserAgent = customUserAgent
217 | }
218 | }
219 |
220 | #if os(iOS)
221 | public struct WebView: UIViewRepresentable {
222 | let config: WebViewConfig
223 | @Binding var action: WebViewAction
224 | @Binding var state: WebViewState
225 | let restrictedPages: [String]?
226 | let htmlInState: Bool
227 | let schemeHandlers: [String: (URL) -> Void]
228 |
229 | public init(config: WebViewConfig = .default,
230 | action: Binding,
231 | state: Binding,
232 | restrictedPages: [String]? = nil,
233 | htmlInState: Bool = false,
234 | schemeHandlers: [String: (URL) -> Void] = [:]) {
235 | self.config = config
236 | _action = action
237 | _state = state
238 | self.restrictedPages = restrictedPages
239 | self.htmlInState = htmlInState
240 | self.schemeHandlers = schemeHandlers
241 | }
242 |
243 | public func makeCoordinator() -> WebViewCoordinator {
244 | WebViewCoordinator(webView: self)
245 | }
246 |
247 | public func makeUIView(context: Context) -> WKWebView {
248 | let preferences = WKPreferences()
249 | preferences.javaScriptEnabled = config.javaScriptEnabled
250 |
251 | let configuration = WKWebViewConfiguration()
252 | configuration.allowsInlineMediaPlayback = config.allowsInlineMediaPlayback
253 | configuration.mediaTypesRequiringUserActionForPlayback = config.mediaTypesRequiringUserActionForPlayback
254 | configuration.preferences = preferences
255 |
256 | let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
257 | webView.navigationDelegate = context.coordinator
258 | webView.uiDelegate = context.coordinator
259 | webView.allowsBackForwardNavigationGestures = config.allowsBackForwardNavigationGestures
260 | webView.scrollView.isScrollEnabled = config.isScrollEnabled
261 | webView.isOpaque = config.isOpaque
262 | if #available(iOS 14.0, *) {
263 | webView.backgroundColor = UIColor(config.backgroundColor)
264 | } else {
265 | webView.backgroundColor = .clear
266 | }
267 |
268 | return webView
269 | }
270 |
271 | @objc func hello() {
272 |
273 | }
274 |
275 | public func updateUIView(_ uiView: WKWebView, context: Context) {
276 | if action == .idle || context.coordinator.actionInProgress {
277 | return
278 | }
279 | context.coordinator.actionInProgress = true
280 | switch action {
281 | case .idle:
282 | break
283 | case .load(let request):
284 | uiView.load(request)
285 | case .loadHTML(let pageHTML):
286 | uiView.loadHTMLString(pageHTML, baseURL: nil)
287 | case .reload:
288 | uiView.reload()
289 | case .goBack:
290 | uiView.goBack()
291 | case .goForward:
292 | uiView.goForward()
293 | case .evaluateJS(let command, let callback):
294 | uiView.evaluateJavaScript(command) { result, error in
295 | if let error = error {
296 | callback(.failure(error))
297 | } else {
298 | callback(.success(result))
299 | }
300 | }
301 | }
302 | }
303 | }
304 | #endif
305 |
306 | #if os(macOS)
307 | public struct WebView: NSViewRepresentable {
308 | let config: WebViewConfig
309 | @Binding var action: WebViewAction
310 | @Binding var state: WebViewState
311 | let restrictedPages: [String]?
312 | let htmlInState: Bool
313 | let schemeHandlers: [String: (URL) -> Void]
314 |
315 | public init(config: WebViewConfig = .default,
316 | action: Binding,
317 | state: Binding,
318 | restrictedPages: [String]? = nil,
319 | htmlInState: Bool = false,
320 | schemeHandlers: [String: (URL) -> Void] = [:]) {
321 | self.config = config
322 | _action = action
323 | _state = state
324 | self.restrictedPages = restrictedPages
325 | self.htmlInState = htmlInState
326 | self.schemeHandlers = schemeHandlers
327 | }
328 |
329 | public func makeCoordinator() -> WebViewCoordinator {
330 | WebViewCoordinator(webView: self)
331 | }
332 |
333 | public func makeNSView(context: Context) -> WKWebView {
334 | let preferences = WKPreferences()
335 | preferences.javaScriptEnabled = config.javaScriptEnabled
336 |
337 | let configuration = WKWebViewConfiguration()
338 | configuration.preferences = preferences
339 |
340 | let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
341 | webView.navigationDelegate = context.coordinator
342 | webView.customUserAgent = config.customUserAgent
343 | webView.uiDelegate = context.coordinator
344 | webView.allowsBackForwardNavigationGestures = config.allowsBackForwardNavigationGestures
345 |
346 | return webView
347 | }
348 |
349 | public func updateNSView(_ uiView: WKWebView, context: Context) {
350 | if action == .idle {
351 | return
352 | }
353 | switch action {
354 | case .idle:
355 | break
356 | case .load(let request):
357 | uiView.load(request)
358 | case .loadHTML(let html):
359 | uiView.loadHTMLString(html, baseURL: nil)
360 | case .reload:
361 | uiView.reload()
362 | case .goBack:
363 | uiView.goBack()
364 | case .goForward:
365 | uiView.goForward()
366 | case .evaluateJS(let command, let callback):
367 | uiView.evaluateJavaScript(command) { result, error in
368 | if let error = error {
369 | callback(.failure(error))
370 | } else {
371 | callback(.success(result))
372 | }
373 | }
374 | }
375 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
376 | action = .idle
377 | }
378 | }
379 | }
380 | #endif
381 |
382 | struct WebViewTest: View {
383 | @State private var action = WebViewAction.idle
384 | @State private var state = WebViewState.empty
385 | @State private var address = "https://www.google.com"
386 |
387 | var body: some View {
388 | VStack {
389 | titleView
390 | navigationToolbar
391 | errorView
392 | Divider()
393 | WebView(action: $action,
394 | state: $state,
395 | restrictedPages: ["apple.com"],
396 | htmlInState: true)
397 | Text(state.pageHTML ?? "")
398 | .lineLimit(nil)
399 | Spacer()
400 | }
401 | }
402 |
403 | private var titleView: some View {
404 | Text(String(format: "%@ - %@", state.pageTitle ?? "Load a page", state.pageURL ?? "No URL"))
405 | .font(.system(size: 24))
406 | }
407 |
408 | private var navigationToolbar: some View {
409 | HStack(spacing: 10) {
410 | Button("Test HTML") {
411 | action = .loadHTML("""
412 |
413 | Hello World!
414 | Go to google
415 |
416 | """)
417 | }
418 | TextField("Address", text: $address)
419 | if state.isLoading {
420 | if #available(iOS 14, macOS 11, *) {
421 | ProgressView()
422 | .progressViewStyle(CircularProgressViewStyle())
423 | } else {
424 | Text("Loading")
425 | }
426 | }
427 | Spacer()
428 | Button("Go") {
429 | if let url = URL(string: address) {
430 | action = .load(URLRequest(url: url))
431 | }
432 | }
433 | Button(action: {
434 | action = .reload
435 | }) {
436 | if #available(iOS 14, macOS 11, *) {
437 | Image(systemName: "arrow.counterclockwise")
438 | .imageScale(.large)
439 | } else {
440 | Text("Reload")
441 | }
442 | }
443 | if state.canGoBack {
444 | Button(action: {
445 | action = .goBack
446 | }) {
447 | if #available(iOS 14, macOS 11, *) {
448 | Image(systemName: "chevron.left")
449 | .imageScale(.large)
450 | } else {
451 | Text("<")
452 | }
453 | }
454 | }
455 | if state.canGoForward {
456 | Button(action: {
457 | action = .goForward
458 | }) {
459 | if #available(iOS 14, macOS 11, *) {
460 | Image(systemName: "chevron.right")
461 | .imageScale(.large)
462 | } else {
463 | Text(">")
464 | }
465 | }
466 | }
467 | }.padding()
468 | }
469 |
470 | private var errorView: some View {
471 | Group {
472 | if let error = state.error {
473 | Text(error.localizedDescription)
474 | .foregroundColor(.red)
475 | }
476 | }
477 | }
478 | }
479 |
480 | struct WebView_Previews: PreviewProvider {
481 | static var previews: some View {
482 | WebViewTest()
483 | }
484 | }
485 |
--------------------------------------------------------------------------------