├── 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 | ![](resource/snapshot.png) 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 | --------------------------------------------------------------------------------