├── readmeImgs └── image.png ├── .env_exmple ├── tech_note ├── 2024-11-30 12.38.59.png ├── tech_note.md └── tech_note_ZH.md ├── ScreenCut ├── ScreenCut │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon 1.png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (1).png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (2).png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (3).png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (4).png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (5).png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (6).png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (1) 1.png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (4) 1.png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (5) 1.png │ │ │ └── Contents.json │ │ ├── logo-img.imageset │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (2).png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (3).png │ │ │ └── Contents.json │ │ └── logo-img-white.imageset │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (7).png │ │ │ ├── 8201365_crop_cropping_cut_ui_multimedia_icon (8).png │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── ScreenCut.entitlements │ ├── MainOverlay │ │ ├── showView │ │ │ ├── OverlayProtocol.swift │ │ │ ├── ScreenshotBaseOverlayView.swift │ │ │ ├── ScreenshotDoodleView.swift │ │ │ ├── ScreenshotCircleView.swift │ │ │ ├── ScreenshotArrowView.swift │ │ │ ├── ScreenshotTextView.swift │ │ │ └── ScreenshotRectangleView.swift │ │ ├── ScreenshotWindow.swift │ │ └── ScrollCapture │ │ │ └── ScreenCaptureHelper.swift │ ├── Info.plist │ ├── Network │ │ ├── RequestNetwork.swift │ │ └── LocalNetworkAPI.swift │ ├── Other │ │ ├── TranslatorView.swift │ │ └── OCRView.swift │ ├── HelpView │ │ ├── ToastView.swift │ │ ├── ToastWindow.swift │ │ ├── AboutView.swift │ │ ├── Toast.swift │ │ └── PreferenceSettingsView.swift │ ├── Action │ │ ├── ScreenCutApp.swift │ │ ├── ShareDataModel.swift │ │ ├── OverLayerExtension.swift │ │ ├── VarExtension.swift │ │ └── ScreenCut.swift │ ├── SwiftUIComponents │ │ ├── AboutWindowController.swift │ │ ├── MigrationGuide.md │ │ ├── SwiftUIScreenshotOverlayView.swift │ │ ├── SwiftUIScreenshotWindow.swift │ │ ├── PreferenceSettingsWindowController.swift │ │ └── SwiftUIIntegration.swift │ ├── AppDelegate.swift │ └── BottomView │ │ └── EditCutBottomView.swift ├── Server │ ├── __pycache__ │ │ ├── translator.cpython-310.pyc │ │ └── translator.cpython-312.pyc │ ├── translate.py │ └── translator.py └── ScreenCut.xcodeproj │ └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── WorkspaceSettings.xcsettings │ └── swiftpm │ └── Package.resolved ├── .gitignore ├── shell ├── third_lib.txt ├── build_local.sh ├── scan_dir_github_remote_link.py ├── create_release.sh └── brew.sh ├── third_lib ├── third_lib.txt └── download_dependencies.py ├── backend ├── translate.py └── translator.py ├── test_appcast.xml ├── .gitmodules ├── appcast.xml ├── appcast_create.py ├── README_ZH.md ├── README.md └── LICENSE /readmeImgs/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/readmeImgs/image.png -------------------------------------------------------------------------------- /.env_exmple: -------------------------------------------------------------------------------- 1 | 2 | GITHUB_TOKEN="" # github 的 access token 3 | TAG=v1.0.9 # 替换自己的tag 4 | ASSET_FILE=./xx/xx.dmg # 替换自己的路径 -------------------------------------------------------------------------------- /tech_note/2024-11-30 12.38.59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/tech_note/2024-11-30 12.38.59.png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ScreenCut/Server/__pycache__/translator.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/Server/__pycache__/translator.cpython-310.pyc -------------------------------------------------------------------------------- /ScreenCut/Server/__pycache__/translator.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/Server/__pycache__/translator.cpython-312.pyc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ScreenCut/ScreenCut.xcodeproj/project.xcworkspace/xcuserdata 2 | ScreenCut/ScreenCut.xcodeproj/xcuserdata 3 | ScreenCut/Builds 4 | ScreenCut/build 5 | 6 | .env -------------------------------------------------------------------------------- /ScreenCut/ScreenCut.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/ScreenCut.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/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 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon 1.png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/logo-img.imageset/8201365_crop_cropping_cut_ui_multimedia_icon (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/logo-img.imageset/8201365_crop_cropping_cut_ui_multimedia_icon (2).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/logo-img.imageset/8201365_crop_cropping_cut_ui_multimedia_icon (3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/logo-img.imageset/8201365_crop_cropping_cut_ui_multimedia_icon (3).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (1).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (2).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (3).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (4).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (4).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (5).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (5).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (6).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (6).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (1) 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (1) 1.png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (4) 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (4) 1.png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (5) 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/8201365_crop_cropping_cut_ui_multimedia_icon (5) 1.png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/logo-img-white.imageset/8201365_crop_cropping_cut_ui_multimedia_icon (7).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/logo-img-white.imageset/8201365_crop_cropping_cut_ui_multimedia_icon (7).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/logo-img-white.imageset/8201365_crop_cropping_cut_ui_multimedia_icon (8).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VCBSstudio/ScreenCut/HEAD/ScreenCut/ScreenCut/Assets.xcassets/logo-img-white.imageset/8201365_crop_cropping_cut_ui_multimedia_icon (8).png -------------------------------------------------------------------------------- /ScreenCut/ScreenCut.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/MainOverlay/showView/OverlayProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayProtocol.swift 3 | // TestMap 4 | // 5 | // Created by helinyu on 2024/10/27. 6 | // 7 | 8 | import AppKit 9 | import Foundation 10 | import SwiftUI 11 | 12 | protocol OverlayProtocol { 13 | var selectedColor: NSColor { get set } 14 | var lineWidth: CGFloat { get set } 15 | } 16 | -------------------------------------------------------------------------------- /shell/third_lib.txt: -------------------------------------------------------------------------------- 1 | git@github.com:RxSwift/RxSwift.git 2 | git@github.com:ReactiveSwift/ReactiveSwift.git 3 | git@github.com:Alamofire/Alamofire.git 4 | git@github.com:nvzqz/FileKit.git 5 | git@github.com:sparkle-project/Sparkle.git 6 | git@github.com:sindresorhus/KeyboardShortcuts.git 7 | git@github.com:Moya/Moya.git 8 | git@github.com:pointfreeco/swift-composable-architecture.git -------------------------------------------------------------------------------- /third_lib/third_lib.txt: -------------------------------------------------------------------------------- 1 | git@github.com:ReactiveCocoa/ReactiveSwift.git 2 | git@github.com:ReactiveX/RxSwift.git 3 | git@github.com:Alamofire/Alamofire.git 4 | git@github.com:nvzqz/FileKit.git 5 | git@github.com:sparkle-project/Sparkle.git 6 | git@github.com:sindresorhus/KeyboardShortcuts.git 7 | git@github.com:Moya/Moya.git 8 | git@github.com:pointfreeco/swift-composable-architecture.git -------------------------------------------------------------------------------- /backend/translate.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | import translator 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/translate', methods=['POST']) 7 | def translate(): 8 | text = request.json['text'] 9 | # 进行翻译逻辑 10 | translated_text = translator.translate(text) 11 | return jsonify({'translated_text': translated_text}) 12 | 13 | if __name__ == '__main__': 14 | app.run() 15 | -------------------------------------------------------------------------------- /tech_note/tech_note.md: -------------------------------------------------------------------------------- 1 | # Steps for Packaging and Uploading a Release 2 | 1. Update the **Version** and **Build** numbers in Xcode.
3 | 2. Execute: `move .env_example .env`, modify the variables in the .env file.
4 | 3. Update the tag version and execute the following command:
5 | ```bash 6 | ./build_local.sh && ./create_release.sh 7 | ```
8 | 4. Update information to the brew repository, execute ./brew.sh 9 | -------------------------------------------------------------------------------- /ScreenCut/Server/translate.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | import translator 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/translate', methods=['POST']) 7 | def translate(): 8 | print("Received request data:", request.json) 9 | text = request.json['text'] 10 | # 进行翻译逻辑 11 | translated_text = translator.translate(text) 12 | return jsonify({'translated_text': translated_text}) 13 | 14 | if __name__ == '__main__': 15 | app.run( port=5000) 16 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/logo-img.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (2).png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (3).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 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/logo-img-white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (7).png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (8).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 | -------------------------------------------------------------------------------- /tech_note/tech_note_ZH.md: -------------------------------------------------------------------------------- 1 | # 打包和上传Release的步骤 2 | 1. 在 Xcode 中更新 **Version** 和 **Build** 版本号。
3 | 2. 执行:`move .env_exmaple .env` ,修改.env中文件的变量
4 | 3. 设置编译版本:
5 | 1)xcode中设置编译版本号: Version版本修改, build的数字+1
6 | 2)新增加tag标签: git tag v1.1.3 && git push origin --tags
7 | 3)在 .env文件中设置tag的值,和创建的tag版本一样
8 | 4. 执行以下命令:
9 | ```bash 10 | ./shell/build_local.sh && ./shell/create_release.sh && ./shell/brew.sh 11 | ```
12 | 1) 编译项目 2)创建到release的文件 3)创建brew文件
13 | 14 | 15 | 16 | 17 | # 项目依赖本地 18 | 路径 : Third_lib/xxx
19 | [依赖库的图片显示](./2024-11-30 12.38.59.png) 20 | 21 | 22 | -------------------------------------------------------------------------------- /backend/translator.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from transformers import MarianMTModel, MarianTokenizer 3 | 4 | # 下载模型和标记器 5 | model_name = "Helsinki-NLP/opus-mt-en-zh" 6 | tokenizer = MarianTokenizer.from_pretrained(model_name) 7 | model = MarianMTModel.from_pretrained(model_name) 8 | 9 | def translate(text): 10 | # 将输入文本编码 11 | inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True) 12 | 13 | # 生成翻译 14 | with torch.no_grad(): 15 | outputs = model.generate(input_ids=inputs['input_ids'], attention_mask=inputs['attention_mask']) 16 | # 解码输出文本 17 | return tokenizer.decode(outputs[0], skip_special_tokens=True) 18 | -------------------------------------------------------------------------------- /ScreenCut/Server/translator.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from transformers import MarianMTModel, MarianTokenizer 3 | 4 | # 下载模型和标记器 5 | model_name = "Helsinki-NLP/opus-mt-en-zh" 6 | tokenizer = MarianTokenizer.from_pretrained(model_name) 7 | model = MarianMTModel.from_pretrained(model_name) 8 | 9 | def translate(text): 10 | # 将输入文本编码 11 | inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True) 12 | 13 | # 生成翻译 14 | with torch.no_grad(): 15 | outputs = model.generate(input_ids=inputs['input_ids'], attention_mask=inputs['attention_mask']) 16 | 17 | # 解码输出文本 18 | return tokenizer.decode(outputs[0], skip_special_tokens=True) 19 | -------------------------------------------------------------------------------- /test_appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ScreenCut 1.0.3 6 | 1.0.3 7 | https://github.com/VCBSstudio/ScreenCut/releases/tag/1.0.3 8 | Sat, 02 Nov 2024 15:21:30 GMT 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /third_lib/download_dependencies.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import subprocess 3 | import os 4 | 5 | def clone_github_repositories(file_path): 6 | with open(file_path, 'r') as file: 7 | for line in file: 8 | # 去除行尾的换行符 9 | line = line.strip() 10 | if line.startswith('git@github.com:'): 11 | # 使用 git clone 命令克隆仓库 12 | try: 13 | subprocess.run(['git', 'clone', line], check=True) 14 | print(f'Successfully cloned {line}') 15 | except subprocess.CalledProcessError as e: 16 | print(f'Failed to clone {line}: {e}') 17 | 18 | # 调用函数,传入文件路径 19 | clone_github_repositories('./third_lib.txt') -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | NSScreenCaptureUsageDescription 11 | 我们需要访问屏幕录制以提供直播或录制功能 12 | SUFeedURL 13 | https://github.com/VCBSstudio/ScreenCut/blob/main/appcast.xml 14 | SUPublicEDKey 15 | zd7Bjskp7tfkf7TpxCdcexgCh71tsKvYwecV/Loe16Y= 16 | NSMicrophoneUsageDescription 17 | 访问麦克风 18 | NSCameraUsageDescription 19 | 访问相机 20 | 21 | 22 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_lib/ReactiveSwift"] 2 | path = third_lib/ReactiveSwift 3 | url = git@github.com:ReactiveCocoa/ReactiveSwift.git 4 | [submodule "third_lib/KeyboardShortcuts"] 5 | path = third_lib/KeyboardShortcuts 6 | url = git@github.com:sindresorhus/KeyboardShortcuts.git 7 | [submodule "third_lib/Moya"] 8 | path = third_lib/Moya 9 | url = git@github.com:Moya/Moya.git 10 | [submodule "third_lib/FileKit"] 11 | path = third_lib/FileKit 12 | url = git@github.com:nvzqz/FileKit.git 13 | [submodule "third_lib/Alamofire"] 14 | path = third_lib/Alamofire 15 | url = git@github.com:Alamofire/Alamofire.git 16 | [submodule "third_lib/Sparkle"] 17 | path = third_lib/Sparkle 18 | url = git@github.com:sparkle-project/Sparkle.git 19 | [submodule "third_lib/RxSwift"] 20 | path = third_lib/RxSwift 21 | url = git@github.com:ReactiveX/RxSwift.git 22 | [submodule "third_lib/swift-composable-architecture"] 23 | path = third_lib/swift-composable-architecture 24 | url = git@github.com:pointfreeco/swift-composable-architecture.git 25 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Network/RequestNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestNetwork.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/11/7. 6 | // 7 | 8 | 9 | import Moya 10 | import Combine 11 | import Foundation 12 | 13 | // 定义 API 14 | let provider = MoyaProvider() 15 | var networkcancellables = Set() 16 | 17 | func LocalRequestPublisher(_ target: T, responseType: R.Type) -> AnyPublisher { 18 | Future { promise in 19 | provider.request(target as! LocalNetworkAPI) { result in 20 | switch result { 21 | case .success(let response): 22 | do { 23 | let decodedData = try JSONDecoder().decode(R.self, from: response.data) 24 | promise(.success(decodedData)) 25 | } catch { 26 | promise(.failure(error)) 27 | } 28 | case .failure(let error): 29 | promise(.failure(error)) 30 | } 31 | } 32 | } 33 | .eraseToAnyPublisher() 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/MainOverlay/showView/ScreenshotBaseOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenshotBaseOverlayView.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/11/3. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | 12 | class ScreenshotBaseOverlayView: NSView, OverlayProtocol { 13 | var selectedColor: NSColor = .white 14 | var lineWidth: CGFloat = 2.0 15 | var editFinished = false; 16 | 17 | func handleborderForPoint(_ point: NSPoint) -> RetangleResizeHandle { 18 | return .none 19 | } 20 | 21 | func isOnBorderAt(_ point: NSPoint) -> Bool { 22 | let handle = self.handleborderForPoint(point) 23 | if handle == .none { 24 | return false 25 | } 26 | return true 27 | } 28 | 29 | func handleForPoint(_ point: NSPoint) -> RetangleResizeHandle { 30 | return .none 31 | } 32 | 33 | override init(frame frameRect: NSRect) { 34 | super.init(frame: frameRect) 35 | self.wantsLayer = true 36 | self.layer?.masksToBounds = true 37 | } 38 | 39 | required init?(coder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | 43 | } 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Network/LocalNetworkAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalNetworkAPI.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/11/7. 6 | // 7 | 8 | import Moya 9 | import Combine 10 | import Foundation 11 | 12 | enum LocalNetworkAPI { 13 | case translate(text: String) 14 | } 15 | 16 | extension LocalNetworkAPI: TargetType { 17 | var method: Moya.Method { 18 | return .post 19 | } 20 | 21 | var task: Moya.Task { 22 | switch self { 23 | case .translate(let text): 24 | let parameters = ["text": text] 25 | return .requestParameters(parameters: parameters, encoding: JSONEncoding.default) 26 | } 27 | } 28 | 29 | var headers: [String : String]? { 30 | return ["Content-Type": "application/json"] 31 | } 32 | 33 | var baseURL: URL { 34 | return URL(string: "http://127.0.0.1:5000")! 35 | } 36 | 37 | var path: String { 38 | switch self { 39 | case .translate: 40 | return "/translate" 41 | } 42 | } 43 | } 44 | 45 | 46 | //let provider = MoyaProvider() 47 | //var cancellables = Set() // 用于存储订阅 48 | 49 | // 定义一个用户模型 50 | struct Translate: Codable { 51 | let text: String 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /shell/build_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 配置变量 4 | PROJECT_PATH="./ScreenCut" # 替换为你的项目路径 5 | SCHEME="ScreenCut" # 替换为你的项目 Scheme 名称 6 | CONFIGURATION="Release" # 构建配置,如 Debug 或 Release 7 | OUTPUT_DIR="./Builds" # 打包输出目录 8 | DMG_NAME="$OUTPUT_DIR/$SCHEME.dmg" # DMG 文件路径 9 | ZIP_NAME="$OUTPUT_DIR/$SCHEME.zip" # ZIP 文件路径 10 | 11 | # 打包项目 12 | echo "开始打包 macOS 项目..." 13 | 14 | # 进入项目目录 15 | cd $PROJECT_PATH || { echo "无法进入项目目录 $PROJECT_PATH"; exit 1; } 16 | 17 | # 清理项目 18 | echo "清理项目..." 19 | xcodebuild clean -scheme $SCHEME -configuration $CONFIGURATION || { echo "清理失败"; exit 1; } 20 | 21 | # 构建项目 22 | echo "构建项目..." 23 | xcodebuild build \ 24 | -scheme $SCHEME \ 25 | -configuration $CONFIGURATION \ 26 | -derivedDataPath build || { echo "构建失败"; exit 1; } 27 | 28 | # 定位构建输出的 .app 文件 29 | APP_PATH=$(find build -type d -name "$SCHEME.app" | head -n 1) 30 | 31 | if [ -z "$APP_PATH" ]; then 32 | echo "未找到构建的 .app 文件!" 33 | exit 1 34 | fi 35 | 36 | echo "找到 .app 文件:$APP_PATH" 37 | 38 | # 创建输出目录 39 | mkdir -p $OUTPUT_DIR 40 | 41 | # 打包为 ZIP 文件 42 | echo "打包为 ZIP 文件..." 43 | zip -r "$ZIP_NAME" "$APP_PATH" || { echo "ZIP 打包失败"; exit 1; } 44 | echo "ZIP 文件已生成:$ZIP_NAME" 45 | 46 | # 打包为 DMG 文件 47 | echo "创建 DMG 文件..." 48 | hdiutil create -volname "$SCHEME" \ 49 | -srcfolder "$APP_PATH" \ 50 | -ov -format UDZO "$DMG_NAME" || { echo "DMG 创建失败"; exit 1; } 51 | echo "DMG 文件已生成:$DMG_NAME" 52 | 53 | echo "打包完成!文件已保存至:$OUTPUT_DIR" 54 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Other/TranslatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TranslatorView.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/11/2. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class TranslatorViewWindowController : NSWindowController { 11 | 12 | convenience init(transText: String?) { 13 | 14 | let window = NSWindow( 15 | contentRect: NSScreen.main!.frame, 16 | styleMask: [.titled, .closable, .resizable], 17 | backing: .buffered, 18 | defer: false 19 | ) 20 | 21 | window.center() 22 | window.setFrameAutosaveName("翻译") 23 | window.title = "翻译" 24 | window.level = .screenSaver 25 | window.contentView = NSHostingView(rootView: TranslatorView(resultText: transText)) 26 | 27 | self.init(window: window) 28 | } 29 | 30 | override func showWindow(_ sender: Any?) { 31 | super.showWindow(sender) 32 | } 33 | } 34 | 35 | struct TranslatorView: View { 36 | var resultText: String? 37 | var body: some View { 38 | VStack { 39 | Button("拷贝") { 40 | let paste = NSPasteboard.general 41 | paste.clearContents() 42 | paste.setString(resultText ?? "", forType: .string) 43 | ToastWindow(message:"拷贝成功").showToast() 44 | 45 | } 46 | Text(resultText ?? "没有文字内容") 47 | .padding(EdgeInsets(top: 40, leading: 40, bottom: 40, trailing: 40)) 48 | } 49 | } 50 | } 51 | 52 | #Preview { 53 | OCRView() 54 | } 55 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ScreenCut 5 | 6 | 1.0.3 7 | Sat, 02 Nov 2024 15:21:30 GMT 8 | 103 9 | 1.0.3 10 | 12.5 11 | 12 | 13 | 14 | 1.0.2 15 | Sat, 02 Nov 2024 12:20:37 GMT 16 | 101 17 | 1.0.2 18 | 12.5 19 | 20 | 21 | 22 | 1.0.1 23 | Sat, 02 Nov 2024 12:20:37 GMT 24 | 101 25 | 1.0.1 26 | 12.5 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/HelpView/ToastView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastView.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/11/10. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct ToastView: View { 12 | var message: String 13 | 14 | var body: some View { 15 | Text(message) 16 | .padding() 17 | .background(Color.black.opacity(0.8)) 18 | .foregroundColor(.white) 19 | .cornerRadius(10) 20 | .shadow(radius: 10) 21 | .transition(.opacity) 22 | } 23 | } 24 | 25 | struct ShowToastView: View { 26 | @State private var showToast = false 27 | @State private var toastMessage = "" 28 | 29 | var body: some View { 30 | ZStack { 31 | // VStack { 32 | // Text("Hello, SwiftUI!") 33 | // Button("Show Toast") { 34 | // showToastMessage("This is a Toast message!") 35 | // } 36 | // } 37 | 38 | // 当 showToast 为 true 时显示 Toast 39 | if showToast { 40 | ToastView(message: toastMessage) 41 | .padding() 42 | .onAppear { 43 | // 自动隐藏 Toast,延迟2秒 44 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 45 | withAnimation { 46 | showToast = false 47 | } 48 | } 49 | } 50 | } 51 | } 52 | .animation(.easeInOut, value: showToast) 53 | } 54 | 55 | func showToastMessage(_ message: String) { 56 | toastMessage = message 57 | withAnimation { 58 | showToast = true 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /appcast_create.py: -------------------------------------------------------------------------------- 1 | import os 2 | import xml.etree.ElementTree as ET 3 | from datetime import datetime 4 | 5 | def get_file_size(file_path): 6 | return os.path.getsize(file_path) 7 | 8 | def create_item(app_name, version, download_url, release_notes_url, file_path): 9 | file_size = get_file_size(file_path) 10 | 11 | item = ET.Element('item') 12 | 13 | title = ET.SubElement(item, 'title') 14 | title.text = f"{app_name} {version}" 15 | 16 | version_element = ET.SubElement(item, 'sparkle:version') 17 | version_element.text = version 18 | 19 | release_notes_element = ET.SubElement(item, 'sparkle:releaseNotesLink') 20 | release_notes_element.text = release_notes_url 21 | 22 | # 动态生成当前日期 23 | pub_date = ET.SubElement(item, 'pubDate') 24 | pub_date.text = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') 25 | 26 | enclosure = ET.SubElement(item, 'enclosure') 27 | enclosure.set('url', download_url) 28 | enclosure.set('sparkle:version', version) 29 | enclosure.set('length', str(file_size)) 30 | enclosure.set('type', 'application/octet-stream') 31 | 32 | return item 33 | 34 | # 示例使用 35 | app_name = "ScreenCut" 36 | version = "1.0.3" 37 | download_url = "https://github.com/VCBSstudio/ScreenCut/releases/download/1.0.3/ScreenCut.1.0.3.dmg" 38 | release_notes_url = "https://github.com/VCBSstudio/ScreenCut/releases/tag/1.0.3" 39 | file_path = "/Users/helinyu/workspace/GitHub/dmg_dir/ScreenCut 1.0.3.dmg" # 替换为实际路径 40 | 41 | item_element = create_item(app_name, version, download_url, release_notes_url, file_path) 42 | 43 | # 将生成的 XML 输出到 appcast.xml 44 | appcast = ET.Element('rss', xmlns='http://www.andymatuschak.org/xml-namespaces/sparkle') 45 | channel = ET.SubElement(appcast, 'channel') 46 | channel.append(item_element) 47 | 48 | tree = ET.ElementTree(appcast) 49 | tree.write('test_appcast.xml', encoding='utf-8', xml_declaration=True) 50 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon 1.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (1).png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (1) 1.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (2).png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (3).png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (4).png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (4) 1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (5).png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (5) 1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "8201365_crop_cropping_cut_ui_multimedia_icon (6).png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/MainOverlay/ScreenshotWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenshotWindow.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/11/3. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | func findCurrentScreen() -> CGDirectDisplayID? { 12 | let mouseLocation = NSEvent.mouseLocation 13 | let screens = NSScreen.screens 14 | for screen in screens { 15 | let screenFrame = screen.frame 16 | if screenFrame.contains(mouseLocation) { 17 | let description = screen.deviceDescription 18 | print("Mouse is on screen with ID: \(description)") 19 | return screen.displayID 20 | } 21 | } 22 | return nil 23 | } 24 | 25 | 26 | class ScreenshotWindow: NSWindow { 27 | 28 | var parentView:ScreenshotOverlayView? 29 | 30 | init(_ contentRect: NSRect = NSScreen.main!.frame, backing bufferingType: NSWindow.BackingStoreType = .buffered, defer flag: Bool = false, size: NSSize = NSSize.zero) { 31 | super.init(contentRect: contentRect, styleMask: [ .closable, .borderless], backing: bufferingType, defer: flag) 32 | let rect = NSRect(x: 0, y: 0, width: contentRect.size.width, height: contentRect.size.height) 33 | print("lt --- content Rect : \(contentRect) --- rect : \(rect)") 34 | AppDelegate.shared.screentId = findCurrentScreen() 35 | let overlayView = ScreenshotOverlayView(frame: rect) 36 | self.isOpaque = false 37 | self.hasShadow = false 38 | self.level = .screenSaver - 1 39 | self.title = kAreaSelector 40 | self.backgroundColor = NSColor.clear 41 | self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] 42 | self.isReleasedWhenClosed = false 43 | self.contentView?.addSubview(overlayView) 44 | parentView = overlayView 45 | } 46 | 47 | required init?(coder: NSCoder) { 48 | fatalError("init(coder:) has not been implemented") 49 | } 50 | 51 | override var canBecomeKey: Bool { 52 | return true 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Action/ScreenCutApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenCutApp.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/10/25. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | import KeyboardShortcuts 11 | 12 | extension KeyboardShortcuts.Name { 13 | static let selectedAreaCut = Self("selectedAreaCut") 14 | static let fullScreenCut = Self("fullScreenCut") 15 | } 16 | 17 | @main 18 | struct ScreenCutApp: App { 19 | 20 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 21 | 22 | init() { 23 | // 设置激活策略为 accessory 24 | NSApplication.shared.setActivationPolicy(.accessory) 25 | } 26 | 27 | var body: some Scene { 28 | // 直接使用MenuBar 替代掉主窗口 29 | MenuBarExtra("", systemImage: "scissors"){ 30 | Button("截屏") { 31 | ScreenCut.saveScreenFullImage() 32 | } 33 | .padding() 34 | Button("选择截屏") { 35 | NSCursor.crosshair.set() 36 | SwiftUIScreenshotWindowController().showWindow(nil) 37 | } 38 | Divider() 39 | Button("偏好设置") { 40 | let preferenceWindowController = PreferenceSettingsWindowController() 41 | preferenceWindowController.showWindow(nil) 42 | } 43 | .padding() 44 | Button("关于"){ 45 | let aboutWindowController = AboutWindowController() 46 | aboutWindowController.showWindow(nil) 47 | } 48 | .padding() 49 | Divider() 50 | Button("退出") { 51 | NSApplication.shared.terminate(nil) 52 | } 53 | .keyboardShortcut("Q", modifiers: [.command]) 54 | } 55 | 56 | } 57 | } 58 | 59 | extension Scene { 60 | func myWindowIsContentResizable() -> some Scene { 61 | if #available(macOS 13.0, *) { 62 | return self.windowResizability(.contentSize) 63 | } 64 | else { 65 | return self 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/HelpView/ToastWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastWindow.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/11/10. 6 | // 7 | 8 | import SwiftUI 9 | import Cocoa 10 | 11 | class ToastWindow: NSWindow { 12 | init(message: String) { 13 | 14 | let toastHeight: CGFloat = 50 15 | let toastWidth: CGFloat = 300 16 | 17 | // 设置 Toast Window 的大小和位置 18 | let screenSize = NSScreen.main?.frame.size ?? CGSize(width: 800, height: 600) 19 | let toastPosition = CGPoint(x: (screenSize.width - toastWidth) / 2, y: screenSize.height - toastHeight - 100) 20 | 21 | // 创建一个无边框、透明的窗口 22 | let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel] 23 | super.init(contentRect: NSRect(x: toastPosition.x, y: toastPosition.y, width: toastWidth, height: toastHeight), 24 | styleMask: styleMask, 25 | backing: .buffered, 26 | defer: false) 27 | 28 | self.isOpaque = false 29 | self.backgroundColor = NSColor.black.withAlphaComponent(0.8) 30 | self.level = .screenSaver + 1 31 | self.hasShadow = true 32 | self.isReleasedWhenClosed = false 33 | self.isMovableByWindowBackground = false 34 | 35 | // 创建并设置显示的文本 36 | let textLabel = NSTextField(labelWithString: message) 37 | textLabel.font = NSFont.systemFont(ofSize: 14) 38 | textLabel.textColor = .white 39 | textLabel.alignment = .center 40 | textLabel.frame = NSRect(x: 10, y: 10, width: toastWidth - 20, height: toastHeight - 20) 41 | 42 | // 添加文本标签到窗口的内容视图 43 | self.contentView?.addSubview(textLabel) 44 | } 45 | 46 | func showToast() { 47 | DispatchQueue.main.async { 48 | self.makeKeyAndOrderFront(nil) 49 | 50 | // 自动隐藏 Toast,延迟 2 秒 51 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 52 | self.close() 53 | } 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/HelpView/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/10/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AboutView: View { 11 | var body: some View { 12 | VStack(alignment: .leading) { 13 | HStack(alignment: .center) { 14 | Image("logo-img-white") 15 | VStack(alignment: .leading) { 16 | Text("ScreenCut") 17 | .fontWeight(.bold) 18 | .font(.title) 19 | Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0") 20 | .font(.system(size: 14)) 21 | } 22 | Spacer() 23 | } 24 | .frame(width: 540, height: 80.0) 25 | .background(.black) 26 | .foregroundColor(.white) 27 | 28 | VStack(alignment: .leading ) { 29 | Text("你所使用的版本是最新版本") 30 | 31 | Spacer() 32 | Spacer() 33 | Spacer() 34 | Spacer() 35 | 36 | Text("使用本软件意味着你了解并同意遵循服务条款") 37 | Spacer() 38 | Text("软件使用部分开源代码和公共领域代码,并遵循相应的协议。") 39 | Spacer() 40 | Text("helinyu 版权所有 @2024-未来") 41 | .font(.system(size: 12)) 42 | .foregroundColor(.gray) 43 | Spacer() 44 | } 45 | .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0)) 46 | } 47 | .frame(width: 540, height: 200) 48 | } 49 | } 50 | 51 | class AboutWindowController: NSWindowController { 52 | convenience init() { 53 | 54 | let window = NSWindow( 55 | contentRect: NSRect(x: 0, y: 0, width: 540, height: 200), 56 | styleMask: [.titled, .closable, .resizable], 57 | backing: .buffered, 58 | defer: false 59 | ) 60 | 61 | window.center() 62 | window.setFrameAutosaveName("About") 63 | window.level = .screenSaver 64 | window.contentView = NSHostingView(rootView: AboutView()) 65 | 66 | self.init(window: window) 67 | } 68 | 69 | override func showWindow(_ sender: Any?) { 70 | super.showWindow(sender) 71 | } 72 | } 73 | 74 | 75 | #Preview { 76 | AboutView() 77 | } 78 | -------------------------------------------------------------------------------- /shell/scan_dir_github_remote_link.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import re 4 | 5 | # 扫描的根目录 6 | root_dir = '../third_lib/' # 这里修改为你的弹幕目录路径 7 | 8 | # 用于存储所有的 GitHub SSH 链接 9 | github_ssh_links = [] 10 | 11 | # 获取项目 GitHub SSH 地址的函数 12 | def get_github_ssh_link_from_git_config(folder_path): 13 | git_config_path = os.path.join(folder_path, '.git', 'config') 14 | 15 | if os.path.exists(git_config_path): 16 | try: 17 | with open(git_config_path, 'r', encoding='utf-8') as f: 18 | content = f.read() 19 | 20 | # 使用正则匹配 SSH 格式的 URL 21 | match = re.search(r'url = git@github\.com:([\w-]+)/([\w-]+)\.git', content) 22 | if match: 23 | user = match.group(1) 24 | repo = match.group(2) 25 | ssh_url = f'git@github.com:{user}/{repo}.git' 26 | return ssh_url 27 | except Exception as e: 28 | print(f"Error reading {git_config_path}: {e}") 29 | return None 30 | 31 | # 根据目录名推测 GitHub SSH 地址 32 | def get_github_ssh_from_folder_name(folder_name): 33 | # 假设目录名格式为 'user-repo' 或 'repo' 34 | # 你可以根据实际情况调整解析规则 35 | folder_name_parts = folder_name.split('-') 36 | if len(folder_name_parts) > 1: 37 | user = folder_name_parts[0] 38 | repo = '-'.join(folder_name_parts[1:]) 39 | return f'git@github.com:{user}/{repo}.git' 40 | else: 41 | # 如果目录名只包含一个部分,推测为仓库名,可以根据实际情况修改规则 42 | return f'git@github.com:{folder_name}/{folder_name}.git' 43 | 44 | # 遍历根目录并查找每个子目录中的 GitHub SSH 地址 45 | for folder_name in os.listdir(root_dir): 46 | folder_path = os.path.join(root_dir, folder_name) 47 | 48 | # 确保是一个目录 49 | if os.path.isdir(folder_path): 50 | print(f"Scanning folder: {folder_name}") 51 | 52 | # 尝试从 .git/config 文件获取 SSH 地址 53 | ssh_link = get_github_ssh_link_from_git_config(folder_path) 54 | 55 | if not ssh_link: 56 | # 如果没有找到,尝试根据文件夹名推测 SSH 地址 57 | ssh_link = get_github_ssh_from_folder_name(folder_name) 58 | 59 | if ssh_link: 60 | github_ssh_links.append(ssh_link) 61 | 62 | # 去重并输出所有找到的 GitHub SSH 链接 63 | github_ssh_links = list(set(github_ssh_links)) # 去重 64 | 65 | if github_ssh_links: 66 | print(f"Found {len(github_ssh_links)} unique GitHub SSH links:") 67 | for link in github_ssh_links: 68 | print(link) 69 | else: 70 | print("No GitHub SSH links found.") 71 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | ScreenCut 是一个针对 Mac 平台开发的截图和标注工具,提供了丰富的截图及绘制功能,方便用户进行快速操作和分享。以下是它的主要功能和使用方法: 2 | 3 | --- 4 | 5 | ### 安装方法 6 | 使用 [Homebrew](https://brew.sh/) 安装 ScreenCut: 7 | ```bash 8 | brew tap vcbsstudio/tap 9 | brew install --cask ScreenCut 10 | ``` 11 | 12 | --- 13 | 14 | ### 适用系统 15 | 该工具开发基于 **macOS 15**,适配其他 macOS 版本的兼容性可能需要进一步测试。 16 | 17 | --- 18 | 19 | ### 功能特性 20 | 21 | 1. **截图功能** 22 | 直接截图当前屏幕或选定区域。 23 | 24 | 2. **绘制工具** 25 | - 矩形框绘制 26 | - 椭圆形绘制 27 | - 箭头绘制 28 | - 自由涂鸦绘制 29 | 30 | 3. **文本功能** 31 | - 添加文本 32 | - 调整字体大小 33 | 34 | 4. **文字识别 (OCR)** 35 | 选框区域内的文字自动识别,方便提取文字信息。 36 | 37 | 5. **翻译功能** 38 | - 目前支持 **中文翻译为英文** 39 | - 基于大语言模型实现翻译,需要自行配置。 40 | - 配置教程:[参考链接](https://hly-tech.gitbook.io/front-end/front-end/apple/library/coreml/zhi-xing-python-jiao-ben-diao-yong-ai/shi-yong-rest-api) 41 | - 配置代码:[translate.py](./backend/translate.py) 42 | 43 | 6. **样式自定义** 44 | - 可选择字体大小与线条粗细。 45 | - 自定义颜色。 46 | 47 | --- 48 | 49 | ### 快捷键设置 50 | 通过右上角菜单栏图标,进入“偏好设置”进行快捷键配置,例如: 51 | - **Control + X**:触发翻译为英文功能。 52 | 53 | --- 54 | 55 | ### 截图绘制效果预览 56 | 下图展示了 ScreenCut 的截图和绘制功能效果: 57 | 58 | ![截图绘制的效果](./readmeImgs/image.png) 59 | 60 | --- 61 | 62 | 这是一个简洁实用的工具,适合开发者、设计师以及需要快速标注的用户群体。 63 | 64 | --- 65 | 66 | # 开发者 67 | [打包和上传Release的步骤](./tech_note/tech_note_ZH.md) 68 | 69 | 70 | ### PATH:LICENSE 71 | MIT License 72 | 73 | Copyright (c) 2023 vcbsstudio 74 | 75 | Permission is hereby granted, free of charge, to any person obtaining a copy 76 | of this software and associated documentation files (the "Software"), to deal 77 | in the Software without restriction, including without limitation the rights 78 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 79 | copies of the Software, and to permit persons to whom the Software is 80 | furnished to do so, subject to the following conditions: 81 | 82 | The above copyright notice and this permission notice shall be included in all 83 | copies or substantial portions of the Software. 84 | 85 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 86 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 87 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 88 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 89 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 90 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 91 | SOFTWARE. 92 | -------------------------------------------------------------------------------- /shell/create_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 执行操作 eg: ./create_release.sh 4 | 5 | 6 | # 加载 .env 文件 7 | if [ -f .env ]; then 8 | # 使用 source 或 . 9 | source .env 10 | else 11 | echo ".env 文件不存在" 12 | exit 1 13 | fi 14 | 15 | # 检查并解析 TAG 参数 16 | if [ -z "$TAG" ]; then 17 | echo "Usage: TAG=<$TAG> [ASSET_FILE=<$ASSET_FILE>] $0" 18 | exit 1 19 | fi 20 | 21 | # 将 TAG 的值赋值给 TAG_NAME 22 | TAG_NAME="$TAG" 23 | 24 | # 参数设置 25 | ASSET_FILE="${ASSET_FILE:-}" # 资产文件路径,默认为空 26 | # GITHUB_TOKEN="在.env中获取" # 你的 GitHub 个人访问令牌 27 | OWNER="VCBSstudio" # GitHub 用户名或组织名 28 | REPO="ScreenCut" # GitHub 仓库名 29 | RELEASE_NAME="Release $TAG_NAME" # Release 的名称 30 | BODY="This is the release notes for $TAG_NAME" # Release 描述 31 | 32 | # GitHub API URL 33 | API_URL="https://api.github.com/repos/$OWNER/$REPO/releases" 34 | 35 | # 创建 GitHub Release 36 | create_release() { 37 | echo "Creating release for tag: $TAG_NAME..." 38 | response=$(curl -s -X POST "$API_URL" \ 39 | -H "Authorization: token $GITHUB_TOKEN" \ 40 | -H "Content-Type: application/json" \ 41 | -d @- << EOF 42 | { 43 | "tag_name": "$TAG_NAME", 44 | "name": "$RELEASE_NAME", 45 | "body": "$BODY", 46 | "draft": false, 47 | "prerelease": false 48 | } 49 | EOF 50 | ) 51 | 52 | release_id=$(echo "$response" | jq -r '.id // empty') 53 | if [ -z "$release_id" ]; then 54 | echo "Error creating release: $(echo "$response" | jq -r '.message')" 55 | exit 1 56 | fi 57 | echo "Release created successfully with ID: $release_id" 58 | } 59 | 60 | # 上传资产文件 61 | upload_asset() { 62 | if [ -n "$ASSET_FILE" ] && [ -f "$ASSET_FILE" ]; then 63 | asset_name=$(basename "$ASSET_FILE") 64 | UPLOAD_URL="https://uploads.github.com/repos/$OWNER/$REPO/releases/$release_id/assets?name=$asset_name" 65 | 66 | echo "Uploading asset: $asset_name to $UPLOAD_URL..." 67 | response=$(curl -s -X POST "$UPLOAD_URL" \ 68 | -H "Authorization: token $GITHUB_TOKEN" \ 69 | -H "Content-Type: application/octet-stream" \ 70 | --data-binary @"$ASSET_FILE") 71 | 72 | asset_url=$(echo "$response" | jq -r '.browser_download_url // empty') 73 | if [ -n "$asset_url" ]; then 74 | echo "Asset uploaded successfully: $asset_url" 75 | else 76 | echo "Error uploading asset: $(echo "$response" | jq -r '.message')" 77 | fi 78 | elif [ -z "$ASSET_FILE" ]; then 79 | echo "No asset file specified, skipping upload." 80 | else 81 | echo "Error: File not found: $ASSET_FILE" 82 | exit 1 83 | fi 84 | } 85 | 86 | # 主流程 87 | create_release 88 | upload_asset 89 | -------------------------------------------------------------------------------- /shell/brew.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 加载 .env 文件 4 | if [ -f .env ]; then 5 | # 使用 source 或 . 6 | source .env 7 | else 8 | echo ".env 文件不存在" 9 | exit 1 10 | fi 11 | 12 | # 设置变量 13 | REPO_URL="git@github.com:VCBSstudio/homebrew-Tap.git" 14 | LOCAL_DIR="./../homebrew-Tap" 15 | CASK_FILE="Casks/ScreenCut.rb" # 目标文件路径 16 | NEW_VERSION="$TAG" # 新版本号 17 | NEW_URL="https://github.com/VCBSstudio/ScreenCut/releases/download/$NEW_VERSION/ScreenCut.dmg" # 新版本的下载链接 18 | COMMIT_MESSAGE="Update ScreenCut to version $NEW_VERSION" 19 | 20 | FILE_PATH="$ASSET_FILE" # 本地文件路径 21 | DOWNLOAD_URL="$NEW_URL" # 文件下载链接 22 | TEMP_FILE="./temp_download.dmg" # 临时下载文件存储位置 23 | 24 | # sha256值 25 | if [ -f "$FILE_PATH" ]; then 26 | echo "Calculating SHA256 for local file: $FILE_PATH" 27 | SHA256=$(shasum -a 256 "$FILE_PATH" | awk '{print $1}') 28 | else 29 | echo "Local file not found. Downloading from URL: $DOWNLOAD_URL" 30 | curl -o "$TEMP_FILE" "$DOWNLOAD_URL" 31 | if [ $? -eq 0 ]; then 32 | echo "Download complete. Calculating SHA256..." 33 | SHA256=$(shasum -a 256 "$TEMP_FILE" | awk '{print $1}') 34 | rm -f "$TEMP_FILE" # 删除临时文件 35 | else 36 | echo "Download failed. Exiting." 37 | exit 1 38 | fi 39 | fi 40 | NEW_SHA256="$SHA256" # 新的 SHA256 值 41 | echo "SHA256: $NEW_SHA256" 42 | 43 | 44 | # 克隆仓库 45 | if [ ! -d "$LOCAL_DIR" ]; then 46 | echo "Cloning repository..." 47 | git clone "$REPO_URL" "$LOCAL_DIR" 48 | else 49 | echo "Repository already cloned. Pulling latest changes..." 50 | cd "$LOCAL_DIR" && git pull && cd $OLDPWD 51 | fi 52 | 53 | # 修改 cask 文件 54 | echo "Modifying ScreenCut.rb..." 55 | echo "$(dirname "$0")" 56 | cd "$LOCAL_DIR" || exit 57 | 58 | 59 | echo "Current directory: $(pwd)" 60 | echo "new version : $NEW_VERSION" 61 | echo "NEW_SHA256 : $NEW_SHA256" 62 | echo "NEW_URL : $NEW_URL" 63 | ls -l "$CASK_FILE" 64 | 65 | 66 | # 更新版本号 67 | sed -i '' "s/version \".*\"/version \"$NEW_VERSION\"/" "$CASK_FILE" 68 | # sed -i '' "s/^ version \".*\"/ version \"$NEW_VERSION\"/" "$CASK_FILE" 69 | 70 | 71 | # 更新 SHA256 72 | sed -i '' "s/sha256 \".*\"/sha256 \"$NEW_SHA256\"/" "$CASK_FILE" 73 | # sed -i '' "s/^ sha256 \".*\"/ sha256 \"$NEW_SHA256\"/" "$CASK_FILE" 74 | 75 | # 更新下载链接 76 | # sed -i '' "s/url \".*\"/url \"$NEW_URL\"/" "$CASK_FILE" 77 | # sed -i '' "s|^ url \".*\"| url \"$NEW_URL\"|" "$CASK_FILE" 78 | sed -i "" "s|url \"[^\"]*\"|url \"$NEW_URL\"|" $CASK_FILE 79 | 80 | # 检查修改 81 | if git diff --quiet; then 82 | echo "No changes made." 83 | else 84 | echo "Changes detected. Committing and pushing..." 85 | 86 | # 提交和推送更改 87 | git add "$CASK_FILE" 88 | git commit -m "$COMMIT_MESSAGE" 89 | git push 90 | fi 91 | 92 | # 返回初始目录 93 | cd .. 94 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/HelpView/Toast.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Toast.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/11/9. 6 | // 7 | 8 | import Cocoa 9 | 10 | class Toast: NSView { 11 | 12 | // 初始化 Toast 标签 13 | private let textLabel = NSTextField() 14 | 15 | init(message: String) { 16 | super.init(frame: .zero) 17 | 18 | // 设置文字标签的属性 19 | textLabel.stringValue = message 20 | textLabel.isEditable = false 21 | textLabel.isBezeled = false 22 | textLabel.drawsBackground = false 23 | textLabel.textColor = .white 24 | textLabel.alignment = .center 25 | textLabel.font = NSFont.systemFont(ofSize: 14) 26 | 27 | // 设置 Toast 背景样式 28 | wantsLayer = true 29 | layer?.backgroundColor = NSColor.black.withAlphaComponent(0.8).cgColor 30 | layer?.cornerRadius = 8 31 | 32 | addSubview(textLabel) 33 | 34 | // 设置自动布局 35 | textLabel.translatesAutoresizingMaskIntoConstraints = false 36 | NSLayoutConstraint.activate([ 37 | textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10), 38 | textLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10), 39 | textLabel.topAnchor.constraint(equalTo: topAnchor, constant: 5), 40 | textLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5) 41 | ]) 42 | 43 | translatesAutoresizingMaskIntoConstraints = false 44 | } 45 | 46 | required init?(coder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | // 显示 Toast 51 | func show(in parentView: NSView, duration: TimeInterval = 2.0) { 52 | parentView.addSubview(self) 53 | 54 | // 设置 Toast 的位置和大小约束 55 | NSLayoutConstraint.activate([ 56 | centerXAnchor.constraint(equalTo: parentView.centerXAnchor), 57 | bottomAnchor.constraint(equalTo: parentView.bottomAnchor, constant: -50), 58 | widthAnchor.constraint(lessThanOrEqualToConstant: 200) 59 | ]) 60 | 61 | // 初始透明度为 0,渐入显示 62 | alphaValue = 0 63 | NSAnimationContext.runAnimationGroup { context in 64 | context.duration = 0.3 65 | animator().alphaValue = 1 66 | } 67 | 68 | // 在指定时间后淡出并移除 69 | DispatchQueue.main.asyncAfter(deadline: .now() + duration) { 70 | NSAnimationContext.runAnimationGroup({ context in 71 | context.duration = 0.3 72 | self.animator().alphaValue = 0 73 | }) { 74 | self.removeFromSuperview() 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Action/ShareDataModel.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 4 | // ShareDataModel.swift 5 | // TestMap 6 | // 7 | // Created by helinyu on 2024/10/26. 8 | // 9 | import Foundation 10 | import AppKit 11 | import Combine 12 | 13 | 14 | //let kCutTypeChange = "kCutTypeChange" 15 | 16 | class EditCutBottomShareModel: ObservableObject { 17 | static let shared = EditCutBottomShareModel(cutType: .none, sizeType: .Two, textSize: 12, selectColor: .red) 18 | 19 | init(cutType: EditCutBottmType, sizeType: LineWidthType, textSize: Int, selectColor: SelectedColorHandle) { 20 | self.cutType = cutType 21 | self.sizeType = sizeType 22 | self.textSize = textSize 23 | self.selectColor = selectColor 24 | } 25 | 26 | @Published var cutType: EditCutBottmType = .none { 27 | didSet { 28 | NotificationCenter.default.post(name: .kCutTypeChange, object: cutType) 29 | } 30 | } 31 | 32 | @Published var sizeType: LineWidthType = .Two { // 用来设置绘制的宽度大小,也可能是字体大小 33 | didSet { 34 | NotificationCenter.default.post(name: .kDrawSizeTypeChange, object: sizeType) 35 | } 36 | } 37 | 38 | @Published var textSize: Int = 12 { // 写字的颜色 39 | didSet { 40 | NotificationCenter.default.post(name: .kTextSizeTypeChange, object: textSize) 41 | } 42 | } 43 | @Published var selectColor: SelectedColorHandle = .red { 44 | didSet { 45 | NotificationCenter.default.post(name: .kSelectColorTypeChange, object: selectColor) 46 | } 47 | } 48 | } 49 | 50 | class EditActionShareModel: ObservableObject { 51 | static let shared = EditActionShareModel() 52 | 53 | @Published var actionType: EditActionBottmType = .none { 54 | didSet { 55 | switch actionType { 56 | case .ocr: 57 | ScreenCut.showOCR() 58 | case .translate: 59 | ScreenCut.ocrThenTransRequest() 60 | case .cancel, .download: 61 | DispatchQueue.main.async { [self] in 62 | if actionType == .download { 63 | NotificationCenter.default.post(name:.kDownloadClick, object: nil) 64 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 65 | ScreenCut.cutImage() 66 | for w in NSApplication.shared.windows.filter({ $0.title == kAreaSelector || $0.title == kEditImageText}) { w.close() } 67 | } 68 | } 69 | else { 70 | for w in NSApplication.shared.windows.filter({ $0.title == kAreaSelector || $0.title == kEditImageText}) { w.close() } 71 | } 72 | 73 | } 74 | 75 | case .none: 76 | break 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文文档](./README_ZH.md) 2 | 3 | ScreenCut is a screenshot and annotation tool developed for the Mac platform, providing rich screenshot and drawing functions for quick operation and sharing. Here are its main features and how to use it: 4 | --- 5 | ### Installation 6 | Install ScreenCut using [Homebrew](https://brew.sh/): 7 | ```bash 8 | brew tap vcbsstudio/tap 9 | brew install --cask ScreenCut 10 | ``` 11 | --- 12 | ### System 13 | This tool is based on **macOS 15**, compatibility with other macOS versions may require further testing. 14 | `` --- ### Functionality 15 | ### Features 16 | 1. **Screenshot Function** 17 | Directly take a screenshot of the current screen or selected area. 18 | 19 | 2. **Drawing Tools** 20 | - Rectangle drawing 21 | - Ellipse drawing 22 | - Arrow drawing 23 | - Freehand drawing 24 | 25 | 3. **Text Function** 26 | - Adding text 27 | - Adjusting font size 28 | 29 | 4. **Text Recognition (OCR)** 30 | The text in the checkbox area is automatically recognized, making it easy to extract text information. 31 | 5. **Translation Function** 32 | - Currently supports **Chinese to English translation**. 33 | - Translation is realized based on a large language model, which needs to be configured by yourself. 34 | - Configuration tutorial:[reference link](https://hly-tech.gitbook.io/front-end/front-end/apple/library/coreml/zhi-xing-python-jiao-ben-diao-yong-ai/shi-yong-rest-api) 35 | - Configuration code: [translate.py](./backend/translate.py) 36 | 6. **Style customization 37 | - Selectable font size and line thickness. 38 | - Customizable colors. 39 | --- 40 | ### Shortcut Settings 41 | Enter “Preferences” through the menu bar icon in the upper right corner to configure shortcut keys, for example: 42 | - **Control + X**: Trigger translate to English. 43 | **Control + X**: Trigger the translate to English function. 44 | ### Preview of screenshot drawing effect 45 | The following picture shows the effect of ScreenCut's screenshot and drawing functions: 46 | ![Screenshot drawing effect](./readmeImgs/image.png) 47 | ---- 48 | This is a simple and practical tool for developers, designers, and user groups who need to quickly annotate. 49 | --- 50 | # developer 51 | [Steps for packaging and uploading Releases](./tech_note/tech_note_ZH.md) 52 | 53 | 54 | Translated with DeepL.com (free version) 55 | 56 | --- 57 | 58 | ###PATH:LICENSE 59 | MIT License 60 | 61 | Copyright (c) 2023 vcbsstudio 62 | 63 | Permission is hereby granted, free of charge, to any person obtaining a copy 64 | of this software and associated documentation files (the "Software"), to deal 65 | in the Software without restriction, including without limitation the rights 66 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 67 | copies of the Software, and to permit persons to whom the Software is 68 | furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all 71 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Other/OCRView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TranslatorView.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/11/2. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class OCRViewWindowController : NSWindowController { 11 | var cuText: String? 12 | 13 | convenience init(transText: String?) { 14 | // let originX = NSScreen.main!.frame.size.width/2 - 270 15 | // let originY = NSScreen.main!.frame.size.height/2 - 200 16 | let window = NSWindow( 17 | contentRect: NSScreen.main!.frame, 18 | styleMask: [.titled, .closable, .resizable], 19 | backing: .buffered, 20 | defer: false 21 | ) 22 | window.makeKey() 23 | window.center() 24 | window.setFrameAutosaveName("OCR") 25 | window.title = "OCR" 26 | window.level = .screenSaver 27 | window.contentView = NSHostingView(rootView: OCRView(resultText: transText)) 28 | self.init(window: window) 29 | 30 | cuText = transText 31 | } 32 | 33 | override func showWindow(_ sender: Any?) { 34 | super.showWindow(sender) 35 | } 36 | } 37 | 38 | struct OCRView: View { 39 | var resultText: String? 40 | var body: some View { 41 | VStack { 42 | HStack { 43 | Button("拷贝") { 44 | let paste = NSPasteboard.general 45 | paste.clearContents() 46 | paste.setString(resultText ?? "", forType: .string) 47 | DispatchQueue.main.async { 48 | ToastWindow(message:"拷贝成功").showToast() 49 | } 50 | } 51 | Button("翻译") { 52 | if resultText == nil { 53 | return 54 | } 55 | ScreenCut.transforRequest(resultText!).sink { completion in 56 | switch completion { 57 | case .finished: 58 | print("finished") 59 | case .failure(let error): 60 | DispatchQueue.main.async { 61 | ToastWindow(message: error.userInfo.description).showToast() 62 | } 63 | } 64 | } receiveValue: { text in 65 | DispatchQueue.main.async { 66 | TranslatorViewWindowController(transText: text).showWindow(nil) 67 | } 68 | }.store(in: &cancellables) 69 | 70 | } 71 | } 72 | ScrollView { // 使用垂直方向的 ScrollView 73 | Text(resultText ?? "没有文字内容") 74 | .padding() 75 | .frame(maxWidth: .infinity, alignment: .leading) // 左对齐 76 | }.padding(EdgeInsets(top: 50, leading: 40, bottom: 40, trailing: 40)) 77 | 78 | } 79 | 80 | } 81 | } 82 | 83 | #Preview { 84 | OCRView() 85 | } 86 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/SwiftUIComponents/AboutWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutWindowController.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/12/19. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | 11 | struct SwiftUIAboutView: View { 12 | var body: some View { 13 | VStack(alignment: .leading, spacing: 0) { 14 | // Header 15 | HStack(alignment: .center) { 16 | Image("logo-img-white") 17 | .resizable() 18 | .frame(width: 60, height: 60) 19 | 20 | VStack(alignment: .leading, spacing: 4) { 21 | Text("ScreenCut") 22 | .font(.title) 23 | .fontWeight(.bold) 24 | .foregroundColor(.white) 25 | 26 | Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0") 27 | .font(.system(size: 14)) 28 | .foregroundColor(.white) 29 | } 30 | 31 | Spacer() 32 | } 33 | .frame(height: 80) 34 | .frame(maxWidth: .infinity) 35 | .background(Color.black) 36 | .padding(.horizontal, 20) 37 | 38 | // Content 39 | VStack(alignment: .leading, spacing: 16) { 40 | Text("你所使用的版本是最新版本") 41 | .font(.system(size: 14)) 42 | 43 | Spacer() 44 | 45 | VStack(alignment: .leading, spacing: 8) { 46 | Text("使用本软件意味着你了解并同意遵循服务条款") 47 | .font(.system(size: 12)) 48 | 49 | Text("软件使用部分开源代码和公共领域代码,并遵循相应的协议。") 50 | .font(.system(size: 12)) 51 | 52 | Text("helinyu 版权所有 @2024-未来") 53 | .font(.system(size: 12)) 54 | .foregroundColor(.gray) 55 | } 56 | } 57 | .padding(.horizontal, 20) 58 | .padding(.vertical, 16) 59 | 60 | Spacer() 61 | } 62 | .frame(width: 540, height: 200) 63 | .background(Color.white) 64 | } 65 | } 66 | 67 | class SwiftUIAboutWindowController: NSWindowController { 68 | convenience init() { 69 | let window = NSWindow( 70 | contentRect: NSRect(x: 0, y: 0, width: 540, height: 200), 71 | styleMask: [.titled, .closable, .resizable], 72 | backing: .buffered, 73 | defer: false 74 | ) 75 | 76 | window.center() 77 | window.setFrameAutosaveName("About") 78 | window.level = .screenSaver 79 | window.contentView = NSHostingView(rootView: SwiftUIAboutView()) 80 | 81 | self.init(window: window) 82 | } 83 | 84 | override func showWindow(_ sender: Any?) { 85 | super.showWindow(sender) 86 | } 87 | } 88 | 89 | #Preview { 90 | AboutView() 91 | } -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Action/OverLayerExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverLayerExtension.swift 3 | // TestMap 4 | // 5 | // Created by helinyu on 2024/10/26. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import SwiftUI 11 | 12 | enum RetangleResizeHandle: CaseIterable { 13 | case none 14 | case topLeft, top, topRight, right, bottomRight, bottom, bottomLeft, left 15 | } 16 | 17 | enum SelectedColorHandle: String, CaseIterable, Identifiable { 18 | case red, yellow, green, blue, gray, white 19 | var id: Self { self } 20 | var nsColor: NSColor { 21 | switch self { 22 | case .red: 23 | return NSColor.red 24 | case .yellow: 25 | return NSColor.yellow 26 | case .green: 27 | return NSColor.green 28 | case .blue: 29 | return NSColor.blue 30 | case .gray: 31 | return NSColor.gray 32 | case .white: 33 | return NSColor.white 34 | } 35 | } 36 | 37 | var swiftColor : Color { 38 | switch self { 39 | case .red: 40 | return Color.red 41 | case .yellow: 42 | return Color.yellow 43 | case .green: 44 | return Color.green 45 | case .blue: 46 | return Color.blue 47 | case .gray: 48 | return Color.gray 49 | case .white: 50 | return Color.white 51 | } 52 | } 53 | } 54 | 55 | enum EditCutBottmType: CaseIterable,Identifiable { 56 | 57 | case none 58 | case square, circle, arrow, doodle, text 59 | 60 | static var allCases: [EditCutBottmType] = [square, circle, arrow, doodle, text] 61 | 62 | var id: Self { self } 63 | 64 | var imgName: String { 65 | switch self { 66 | case .square: 67 | return "square" 68 | case .circle: 69 | return "circle" 70 | case .arrow: 71 | return "arrow.up.forward" 72 | case .doodle: 73 | return "pencil.line" 74 | case .text: 75 | return "t.square.fill" 76 | default: 77 | return "" 78 | } 79 | } 80 | } 81 | 82 | enum EditActionBottmType: CaseIterable,Identifiable { 83 | 84 | case none 85 | case ocr, translate, cancel, download 86 | 87 | static var allCases: [EditActionBottmType] = [ocr, translate, cancel, download] 88 | 89 | var id: Self { self } 90 | 91 | var imgName: String { 92 | switch self { 93 | case .ocr: 94 | return "text.document" 95 | case .translate: 96 | return "translate" 97 | case .cancel: 98 | return "xmark" 99 | case .download: 100 | return "square.and.arrow.down" 101 | default: 102 | return "" 103 | } 104 | } 105 | } 106 | 107 | enum LineWidthType: Int, CaseIterable, Identifiable { 108 | 109 | case Two = 2 110 | case Four = 4 111 | case Six = 6 112 | 113 | var id: Self { self } 114 | 115 | var imgName: String { 116 | switch self { 117 | case .Two: 118 | return "circlebadge.fill" 119 | case .Four: 120 | return "circlebadge.fill" 121 | case .Six: 122 | return "circlebadge.fill" 123 | } 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AreaSelector.swift 3 | // VCB 4 | // 5 | // Created by helinyu on 2024/10/23. 6 | // 7 | 8 | 9 | import Foundation 10 | import SwiftUI 11 | import ScreenCaptureKit 12 | import AppKit 13 | import KeyboardShortcuts 14 | import Sparkle 15 | 16 | var defaultSavepath:String = ""; 17 | 18 | extension NSScreen { 19 | var displayID: CGDirectDisplayID? { 20 | return deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID 21 | } 22 | } 23 | 24 | extension SCDisplay { 25 | var nsScreen: NSScreen? { 26 | return NSScreen.screens.first(where: { $0.displayID == self.displayID }) 27 | } 28 | } 29 | 30 | class AppDelegate : NSObject, NSApplicationDelegate { 31 | 32 | 33 | var screentId: CGDirectDisplayID? 34 | 35 | var isResizing = false 36 | static let shared = AppDelegate() 37 | var updaterController: SPUStandardUpdaterController! // 更新 38 | 39 | @AppStorage(kSelectedSavePath) private var selectedPath: String = defaultSavepath 40 | 41 | func applicationDidFinishLaunching(_ notification: Notification) { 42 | 43 | Task { 44 | await ScreenCut.updateScreenContent() 45 | } 46 | 47 | defaultSavepath = VarExtension.createTargetDirIfNotExit() // 先创建目录 48 | if (self.selectedPath.count == 0) { 49 | self.selectedPath = defaultSavepath 50 | } 51 | // print("lt --- selctePath: \(self.selectedPath)") 52 | 53 | KeyboardShortcuts.onKeyDown(for: .selectedAreaCut) {[] in 54 | NSCursor.crosshair.set() 55 | SwiftUIScreenshotWindowController().showWindow(nil) 56 | } 57 | 58 | updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: self, userDriverDelegate: self) 59 | NotificationCenter.default.addObserver(self, selector: #selector(onCheckUpdate), name: Notification.Name("update.app.noti"), object: nil) 60 | 61 | 62 | // 这个暂时先不处理 63 | // DispatchQueue.main.asyncAfter(deadline: .now() + 5) { 64 | // // let captureHelper = ScreenCaptureHelper() 65 | // // captureHelper.startCapturing(scrollHeight: 600, screenWidth: 1400, screenHeight: 3000) 66 | // 67 | // 68 | // Task { 69 | // let desktopURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first! 70 | // let outputURL = desktopURL.appendingPathComponent("ScreenRecording.mov") 71 | // let capture = ScreenRecorder() 72 | // await capture.startRecording(outputURL: outputURL) 73 | // DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 74 | // capture.stopRecording() 75 | // } 76 | // } 77 | // } 78 | 79 | 80 | } 81 | 82 | @objc func onCheckUpdate(noti: Notification) { 83 | updaterController.checkForUpdates(self) 84 | } 85 | } 86 | 87 | // 有关的代理方法 88 | extension AppDelegate: SPUUpdaterDelegate, SPUStandardUserDriverDelegate { 89 | 90 | func updater(_ updater: SPUUpdater, didExtractUpdate item: SUAppcastItem) { 91 | 92 | } 93 | 94 | func updater(_ updater: SPUUpdater, mayPerform updateCheck: SPUUpdateCheck) throws { 95 | 96 | } 97 | 98 | func updater(_ updater: SPUUpdater, willExtractUpdate item: SUAppcastItem) { 99 | 100 | } 101 | 102 | func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: (any Error)?) { 103 | 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "d1ef317ec25c744dd4d191343028df9cfdc08194ebda14a1056df7f91bb50ed8", 3 | "pins" : [ 4 | { 5 | "identity" : "combine-schedulers", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/pointfreeco/combine-schedulers", 8 | "state" : { 9 | "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", 10 | "version" : "1.0.2" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-case-paths", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/pointfreeco/swift-case-paths", 17 | "state" : { 18 | "revision" : "bc92c4b27f9a84bfb498cdbfdf35d5a357e9161f", 19 | "version" : "1.5.6" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-clocks", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/pointfreeco/swift-clocks", 26 | "state" : { 27 | "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", 28 | "version" : "1.0.5" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-collections", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-collections", 35 | "state" : { 36 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 37 | "version" : "1.1.4" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-concurrency-extras", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras", 44 | "state" : { 45 | "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", 46 | "version" : "1.3.1" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-custom-dump", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 53 | "state" : { 54 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", 55 | "version" : "1.3.3" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-dependencies", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/pointfreeco/swift-dependencies", 62 | "state" : { 63 | "revision" : "5526c8a27675dc7b18d6fa643abfb64bcb200b77", 64 | "version" : "1.6.2" 65 | } 66 | }, 67 | { 68 | "identity" : "swift-identified-collections", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/pointfreeco/swift-identified-collections", 71 | "state" : { 72 | "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", 73 | "version" : "1.1.0" 74 | } 75 | }, 76 | { 77 | "identity" : "swift-navigation", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/pointfreeco/swift-navigation", 80 | "state" : { 81 | "revision" : "6b7f44d218e776bb7a5246efb940440d57c8b2cf", 82 | "version" : "2.4.2" 83 | } 84 | }, 85 | { 86 | "identity" : "swift-perception", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/pointfreeco/swift-perception", 89 | "state" : { 90 | "revision" : "8d52279b9809ef27eabe7d5420f03734528f19da", 91 | "version" : "1.4.1" 92 | } 93 | }, 94 | { 95 | "identity" : "swift-sharing", 96 | "kind" : "remoteSourceControl", 97 | "location" : "https://github.com/pointfreeco/swift-sharing", 98 | "state" : { 99 | "revision" : "974e51b3612317ba5883a73dc9fd9e2409a11f2d", 100 | "version" : "1.1.1" 101 | } 102 | }, 103 | { 104 | "identity" : "swift-syntax", 105 | "kind" : "remoteSourceControl", 106 | "location" : "https://github.com/swiftlang/swift-syntax", 107 | "state" : { 108 | "revision" : "0687f71944021d616d34d922343dcef086855920", 109 | "version" : "600.0.1" 110 | } 111 | }, 112 | { 113 | "identity" : "xctest-dynamic-overlay", 114 | "kind" : "remoteSourceControl", 115 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 116 | "state" : { 117 | "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", 118 | "version" : "1.4.3" 119 | } 120 | } 121 | ], 122 | "version" : 3 123 | } 124 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/SwiftUIComponents/MigrationGuide.md: -------------------------------------------------------------------------------- 1 | # UIKit to SwiftUI Migration Guide 2 | 3 | ## Overview 4 | This document outlines the migration from UIKit components to SwiftUI components in the ScreenCut application. 5 | 6 | ## Components Migrated 7 | 8 | ### 1. ScreenshotWindow → ScreenshotWindowController 9 | **Before (UIKit):** 10 | ```swift 11 | class ScreenshotWindow: NSWindow { 12 | // Complex NSWindow implementation 13 | } 14 | ``` 15 | 16 | **After (SwiftUI):** 17 | ```swift 18 | class ScreenshotWindowController: NSWindowController { 19 | // Simplified controller with SwiftUI hosting 20 | } 21 | ``` 22 | 23 | ### 2. ScreenshotOverlayView → EnhancedScreenshotOverlayView 24 | **Before (UIKit):** 25 | ```swift 26 | class ScreenshotOverlayView: ScreenshotRectangleView { 27 | // Complex NSView with manual drawing 28 | } 29 | ``` 30 | 31 | **After (SwiftUI):** 32 | ```swift 33 | struct EnhancedScreenshotOverlayView: View { 34 | // Declarative SwiftUI implementation 35 | } 36 | ``` 37 | 38 | ### 3. EditCutBottomPanel → BottomEditPanelController 39 | **Before (UIKit):** 40 | ```swift 41 | class EditCutBottomPanel: NSWindow { 42 | // Custom NSWindow for bottom panel 43 | } 44 | ``` 45 | 46 | **After (SwiftUI):** 47 | ```swift 48 | class BottomEditPanelController: NSWindowController { 49 | // SwiftUI-hosted bottom panel 50 | } 51 | ``` 52 | 53 | ### 4. ToastWindow → ToastController 54 | **Before (UIKit):** 55 | ```swift 56 | class ToastWindow: NSWindow { 57 | // Custom NSWindow for toast notifications 58 | } 59 | ``` 60 | 61 | **After (SwiftUI):** 62 | ```swift 63 | class ToastController: ObservableObject { 64 | // SwiftUI-based toast system 65 | } 66 | ``` 67 | 68 | ### 5. Window Controllers → SwiftUI Controllers 69 | **Before (UIKit):** 70 | ```swift 71 | class AboutWindowController: NSWindowController { 72 | // NSHostingView with SwiftUI content 73 | } 74 | ``` 75 | 76 | **After (SwiftUI):** 77 | ```swift 78 | class AboutWindowController: NSWindowController { 79 | // Pure SwiftUI implementation 80 | } 81 | ``` 82 | 83 | ## Key Benefits 84 | 85 | 1. **Declarative UI**: SwiftUI's declarative approach makes the UI code more readable and maintainable 86 | 2. **Automatic Updates**: SwiftUI automatically handles view updates when data changes 87 | 3. **Better Performance**: SwiftUI optimizes rendering and updates 88 | 4. **Modern Architecture**: Uses modern Swift patterns and Combine framework 89 | 5. **Easier Testing**: SwiftUI views are easier to test and preview 90 | 91 | ## Migration Steps 92 | 93 | 1. **Replace ScreenshotWindow**: Update AppDelegate and ScreenCutApp to use ScreenshotWindowController 94 | 2. **Update Overlay Views**: Replace ScreenshotOverlayView with EnhancedScreenshotOverlayView 95 | 3. **Migrate Drawing Views**: Convert NSView-based drawing views to SwiftUI views 96 | 4. **Update Window Controllers**: Use new SwiftUI-based window controllers 97 | 5. **Test Integration**: Ensure all components work together seamlessly 98 | 99 | ## Files Modified 100 | 101 | - `AppDelegate.swift`: Updated to use ScreenshotWindowController 102 | - `ScreenCutApp.swift`: Updated to use new window controllers 103 | - `BottomView/EditCutBottomView.swift`: Already SwiftUI, no changes needed 104 | - `HelpView/AboutView.swift`: Already SwiftUI, no changes needed 105 | - `HelpView/PreferenceSettingsView.swift`: Already SwiftUI, no changes needed 106 | 107 | ## New Files Created 108 | 109 | - `SwiftUIComponents/ScreenshotOverlayView.swift`: New SwiftUI overlay view 110 | - `SwiftUIComponents/ScreenshotWindow.swift`: New SwiftUI window controller 111 | - `SwiftUIComponents/AboutWindowController.swift`: Updated about window controller 112 | - `SwiftUIComponents/PreferenceSettingsWindowController.swift`: Updated preferences controller 113 | - `SwiftUIComponents/SwiftUIIntegration.swift`: Integration manager and enhanced views 114 | 115 | ## Testing Checklist 116 | 117 | - [ ] Screenshot window opens correctly 118 | - [ ] Selection rectangle works properly 119 | - [ ] Drawing tools function correctly 120 | - [ ] Bottom edit panel appears and functions 121 | - [ ] Toast notifications work 122 | - [ ] About window opens and displays correctly 123 | - [ ] Preferences window opens and functions 124 | - [ ] Keyboard shortcuts work 125 | - [ ] All drawing operations work 126 | - [ ] Save functionality works 127 | 128 | ## Notes 129 | 130 | - The migration maintains backward compatibility where possible 131 | - Some UIKit components are still used for system-level operations (NSWindow, NSApplication) 132 | - SwiftUI views are hosted in NSHostingView for seamless integration 133 | - The existing data models and business logic remain unchanged -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/MainOverlay/showView/ScreenshotDoodleView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class ScreenshotDoodleView: ScreenshotBaseOverlayView { 4 | // private var lines: [[NSPoint]] = [] // 存储绘制的线条 5 | private var currentLine: [NSPoint] = [] // 当前正在绘制的线条 6 | // var selectedColor: NSColor = NSColor.white 7 | // var lineWidth: CGFloat = 4.0 8 | var dragIng: Bool = false 9 | var dragLastLoc: NSPoint? 10 | let controlPointColor: NSColor = NSColor.white 11 | var drawExpendEnd: Bool = false 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | override func mouseDown(with event: NSEvent) { 22 | // 开始新的线条 23 | let point = convert(event.locationInWindow, from: nil) 24 | print("lt -- mouseDown point: \(point)") 25 | let isAtPoint = self.isOnBorderAt(point) 26 | print("lt -- is at point: \(isAtPoint)") 27 | if isAtPoint { 28 | if (self.isAtEndPoint(point)) { 29 | self.drawExpendEnd = self.isAtLastEndPoint(point) 30 | } 31 | else { 32 | // 拖拽 33 | self.dragIng = true 34 | self.dragLastLoc = point 35 | } 36 | } 37 | else { 38 | currentLine.append(point) 39 | } 40 | } 41 | 42 | override func mouseDragged(with event: NSEvent) { 43 | let point = convert(event.locationInWindow, from: nil) 44 | if self.dragIng && self.dragLastLoc != nil { 45 | if self.currentLine.count == 0 { return } 46 | var transLine:[NSPoint] = [] 47 | let doodlePoints:[NSPoint] = currentLine 48 | let detaX = point.x - self.dragLastLoc!.x 49 | let detaY = point.y - self.dragLastLoc!.y 50 | for index in 0.. Bool { 117 | return NSPoint.isPointOnDoodleLine(doodlePoints: self.currentLine, pointToCheck: point) 118 | } 119 | 120 | func isAtEndPoint(_ point: NSPoint) -> Bool { 121 | if (currentLine.count < 2) { return false} 122 | return self.isAtFirstEndPoint(point) || self.isAtLastEndPoint(point) 123 | } 124 | 125 | func isAtFirstEndPoint(_ point: NSPoint) -> Bool { 126 | let first = currentLine.first! 127 | return (abs(first.x - point.x) < 4 && abs(first.y - point.y) < 4) 128 | } 129 | 130 | func isAtLastEndPoint(_ point: NSPoint) -> Bool { 131 | let last = currentLine.last! 132 | return (abs(last.x - point.x) < 4 && abs(last.y - point.y) < 4) 133 | } 134 | 135 | override func hitTest(_ point: NSPoint) -> NSView? { 136 | let hitView = super.hitTest(point) 137 | if hitView == self { 138 | return self.superview 139 | } 140 | return hitView 141 | } 142 | } 143 | 144 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/SwiftUIComponents/SwiftUIScreenshotOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIScreenshotOverlayView.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/12/19. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | import Combine 11 | 12 | struct SwiftUIScreenshotOverlayView: View { 13 | @StateObject private var bottomEditItem = EditCutBottomShareModel.shared 14 | @ObservedObject private var actionItem = EditActionShareModel.shared 15 | 16 | @State private var selectionRect = CGRect.zero 17 | @State private var hasSelectionRect = false 18 | @State private var isDragging = false 19 | @State private var dragStartPoint = CGPoint.zero 20 | @State private var isEditFinished = false 21 | @State private var isFindForDown = false 22 | 23 | @State private var cancellables = Set() 24 | @State private var bottomPanelVisible = false 25 | 26 | var body: some View { 27 | ZStack { 28 | // Background overlay 29 | Rectangle() 30 | .fill(Color.gray.opacity(0.3)) 31 | .ignoresSafeArea() 32 | .onTapGesture { location in 33 | if !hasSelectionRect { 34 | startSelection(at: location) 35 | } 36 | } 37 | 38 | // Selection rectangle 39 | if hasSelectionRect { 40 | SwiftUISelectionRectangleView( 41 | rect: selectionRect, 42 | isDragging: isDragging, 43 | onDragChanged: handleSelectionDragChanged, 44 | onDragEnded: handleSelectionDragEnded 45 | ) 46 | } 47 | 48 | // Bottom edit panel 49 | if bottomPanelVisible { 50 | VStack { 51 | Spacer() 52 | EditCutBottomView() 53 | .background(Color.black.opacity(0.7)) 54 | .cornerRadius(8) 55 | .padding() 56 | } 57 | } 58 | } 59 | .onAppear { 60 | setupNotifications() 61 | } 62 | .onDisappear { 63 | cleanup() 64 | } 65 | .focusable() 66 | .onKeyPress { keyPress in 67 | handleKeyPress(keyPress) 68 | } 69 | } 70 | 71 | private func setupNotifications() { 72 | NotificationCenter.default.publisher(for: .kCutTypeChange) 73 | .merge(with: NotificationCenter.default.publisher(for: .kSelectColorTypeChange)) 74 | .merge(with: NotificationCenter.default.publisher(for: .kDrawSizeTypeChange)) 75 | .merge(with: NotificationCenter.default.publisher(for: .kTextSizeTypeChange)) 76 | .sink { notification in 77 | handleNotification(notification) 78 | } 79 | .store(in: &cancellables) 80 | 81 | NotificationCenter.default.publisher(for: .kDownloadClick) 82 | .sink { _ in 83 | handleDownloadClick() 84 | } 85 | .store(in: &cancellables) 86 | } 87 | 88 | private func handleNotification(_ notification: Notification) { 89 | switch notification.name { 90 | case .kCutTypeChange: 91 | isEditFinished = true 92 | case .kSelectColorTypeChange, .kDrawSizeTypeChange, .kTextSizeTypeChange: 93 | // Update current oper view properties 94 | break 95 | default: 96 | break 97 | } 98 | } 99 | 100 | private func handleDownloadClick() { 101 | if !isEditFinished { 102 | isEditFinished = true 103 | } 104 | bottomPanelVisible = false 105 | } 106 | 107 | private func startSelection(at location: CGPoint) { 108 | hasSelectionRect = true 109 | dragStartPoint = location 110 | selectionRect = CGRect(origin: location, size: .zero) 111 | } 112 | 113 | private func handleSelectionDragChanged(_ value: DragGesture.Value) { 114 | if !hasSelectionRect { return } 115 | 116 | let currentPoint = value.location 117 | let minX = min(dragStartPoint.x, currentPoint.x) 118 | let minY = min(dragStartPoint.y, currentPoint.y) 119 | let maxX = max(dragStartPoint.x, currentPoint.x) 120 | let maxY = max(dragStartPoint.y, currentPoint.y) 121 | 122 | selectionRect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) 123 | isDragging = true 124 | } 125 | 126 | private func handleSelectionDragEnded(_ value: DragGesture.Value) { 127 | isDragging = false 128 | if hasSelectionRect && selectionRect.width > 10 && selectionRect.height > 10 { 129 | ScreenCut.screenArea = selectionRect 130 | bottomPanelVisible = true 131 | } 132 | } 133 | 134 | private func handleKeyPress(_ keyPress: KeyPress) -> KeyPress.Result { 135 | switch keyPress.key { 136 | case .return: 137 | actionItem.actionType = .download 138 | return .handled 139 | case .delete: 140 | showToast(message: "删除功能暂未实现") 141 | return .handled 142 | case .escape: 143 | // Close screenshot window 144 | NSApplication.shared.keyWindow?.close() 145 | return .handled 146 | default: 147 | return .ignored 148 | } 149 | } 150 | 151 | private func showToast(message: String) { 152 | ToastController.shared.showToast(message: message) 153 | } 154 | 155 | private func cleanup() { 156 | cancellables.removeAll() 157 | } 158 | } 159 | 160 | // MARK: - Selection Rectangle View 161 | struct SwiftUISelectionRectangleView: View { 162 | let rect: CGRect 163 | let isDragging: Bool 164 | let onDragChanged: (DragGesture.Value) -> Void 165 | let onDragEnded: (DragGesture.Value) -> Void 166 | 167 | var body: some View { 168 | Rectangle() 169 | .stroke(Color.white, lineWidth: 2) 170 | .frame(width: rect.width, height: rect.height) 171 | .position(x: rect.midX, y: rect.midY) 172 | .gesture( 173 | DragGesture(minimumDistance: 0) 174 | .onChanged(onDragChanged) 175 | .onEnded(onDragEnded) 176 | ) 177 | } 178 | } -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/BottomView/EditCutBottomView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // test.swift 3 | // TestMacApp 4 | // 5 | // Created by helinyu on 2024/10/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | let kAreaSelector: String = "Area Selector" 11 | let kEditImageText: String = "编辑图片" 12 | let kBottomEditRowHeight: CGFloat = 45.0 13 | let kBottomEditRowWidth: CGFloat = 340 14 | 15 | class EditCutBottomPanel: NSWindow { 16 | 17 | override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { 18 | super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) 19 | isMovable = false 20 | } 21 | 22 | override func mouseDown(with event: NSEvent) { 23 | return 24 | } 25 | } 26 | 27 | let FirstIconLength: CGFloat = 20 28 | let FirstIconPadding: CGFloat = 5 29 | 30 | struct EditCutBottomView: View { 31 | 32 | // 这个看是否需要修噶 EditCutBottomShareModel.shared ,将它修改为observer类型 33 | @StateObject private var bottomEditItem = EditCutBottomShareModel.shared 34 | @ObservedObject private var actionItem = EditActionShareModel.shared 35 | 36 | private func createShapeImageView(for type: EditCutBottmType) -> some View { 37 | Image(nsImage: NSImage(systemSymbolName: type.imgName, accessibilityDescription: nil) ?? NSImage()) 38 | .resizable() 39 | .scaledToFit() 40 | .frame(width: FirstIconLength, height: FirstIconLength) 41 | .foregroundColor(bottomEditItem.cutType == type ? Color.black : Color.white) 42 | .background(bottomEditItem.cutType == type ? Color.white : Color.black) 43 | .padding(FirstIconPadding) 44 | .cornerRadius(3) 45 | .tag(type.imgName) 46 | } 47 | 48 | private func createActionImageView(for type: EditActionBottmType) -> some View { 49 | Image(nsImage: NSImage(systemSymbolName: type.imgName, accessibilityDescription: nil) ?? NSImage()) 50 | .resizable() 51 | .scaledToFit() 52 | .frame(width: FirstIconLength, height: FirstIconLength) 53 | .foregroundColor(.white) 54 | .padding(FirstIconPadding) 55 | } 56 | 57 | var body: some View { 58 | VStack { 59 | HStack { 60 | Spacer() 61 | ForEach(EditCutBottmType.allCases) { type in 62 | createShapeImageView(for: type) 63 | .onTapGesture { 64 | bottomEditItem.cutType = type 65 | } 66 | } 67 | Divider() 68 | .frame(width: 2, height: 25) // 设置分割线的高度 69 | .background(Color.white.opacity(0.3)) // 设置分割线的颜色 70 | 71 | ForEach(EditActionBottmType.allCases) { type in 72 | createActionImageView(for: type) 73 | .onTapGesture { 74 | actionItem.actionType = type 75 | } 76 | } 77 | Spacer() 78 | }.frame(height: 40.0) 79 | if bottomEditItem.cutType != .none { 80 | SecondEditView() 81 | } 82 | }.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) 83 | .background(Color.black.opacity(0.7)) 84 | .frame(width: kBottomEditRowWidth) 85 | .frame(maxWidth: .infinity) 86 | } 87 | } 88 | 89 | struct SecondEditView: View { 90 | var isText: Bool = false 91 | 92 | @StateObject private var bottomEditItem = EditCutBottomShareModel.shared 93 | 94 | private func createLineWidthImageView(for type: LineWidthType) -> some View { 95 | HStack { 96 | Image(nsImage: NSImage(systemSymbolName: "circlebadge.fill", accessibilityDescription: nil) ?? NSImage()) 97 | .resizable() 98 | .scaledToFit() 99 | .frame(width: CGFloat(type.rawValue) * 4, height: CGFloat(type.rawValue) * 4) 100 | .foregroundColor(.white) 101 | .padding(10) 102 | .background(bottomEditItem.sizeType == type ? Color.white.opacity(0.3) : Color.black) 103 | 104 | }.frame(width: 25, height: 25).cornerRadius(3, antialiased: true) 105 | } 106 | 107 | private func createColorView(for type: SelectedColorHandle) -> some View { 108 | type.swiftColor 109 | .frame(height: 30.0) 110 | .border(type == bottomEditItem.selectColor ? Color.purple: Color.clear, width: 2) 111 | } 112 | 113 | var body: some View { 114 | HStack { 115 | if bottomEditItem.cutType == .text { 116 | HStack { 117 | Picker(" 文字:", selection: $bottomEditItem.textSize) { 118 | ForEach(12...100, id: \.self) { value in 119 | Text("\(value)").tag(value) 120 | } 121 | } 122 | .pickerStyle(DefaultPickerStyle()) // 使用下拉菜单样式 123 | .frame(width: 120) 124 | .background(.clear) 125 | .cornerRadius(5) 126 | .foregroundColor(.white) 127 | .padding() 128 | }.frame(width: 120.0) 129 | } 130 | else { 131 | HStack { 132 | ForEach(LineWidthType.allCases) { type in 133 | createLineWidthImageView(for: type) 134 | .onTapGesture { 135 | self.bottomEditItem.sizeType = type 136 | } 137 | } 138 | }.frame(width: 120.0) 139 | } 140 | Divider() 141 | .frame(width: 2, height: 25) 142 | .background(Color.white.opacity(0.3)) 143 | 144 | HStack { 145 | ForEach(SelectedColorHandle.allCases) { type in 146 | createColorView(for: type) 147 | .onTapGesture { 148 | self.bottomEditItem.selectColor = type 149 | } 150 | } 151 | }.frame(width: 200.0) 152 | }.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) 153 | .background(Color.black.opacity(0.7)) 154 | .frame(width: kBottomEditRowWidth ,height: 40) 155 | .frame(maxWidth: .infinity) 156 | } 157 | } 158 | 159 | #Preview { 160 | SecondEditView() 161 | } 162 | 163 | #Preview { 164 | EditCutBottomView() 165 | } 166 | 167 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/SwiftUIComponents/SwiftUIScreenshotWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenshotWindow.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/12/19. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | 11 | class SwiftUIScreenshotWindowController: NSWindowController { 12 | private var overlayView: SwiftUIScreenshotOverlayView? 13 | private var bottomPanelController: BottomEditPanelController? 14 | 15 | convenience init() { 16 | let screenFrame = NSScreen.main?.frame ?? NSRect(x: 0, y: 0, width: 1920, height: 1080) 17 | 18 | let window = NSWindow( 19 | contentRect: screenFrame, 20 | styleMask: [.closable, .borderless], 21 | backing: .buffered, 22 | defer: false 23 | ) 24 | 25 | window.isOpaque = false 26 | window.hasShadow = false 27 | window.level = .screenSaver - 1 28 | window.title = kAreaSelector 29 | window.backgroundColor = NSColor.clear 30 | window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] 31 | window.isReleasedWhenClosed = false 32 | 33 | self.init(window: window) 34 | 35 | setupOverlayView() 36 | setupNotifications() 37 | } 38 | 39 | private func setupOverlayView() { 40 | let overlayView = SwiftUIScreenshotOverlayView() 41 | let hostingView = NSHostingView(rootView: overlayView) 42 | hostingView.frame = window?.contentView?.bounds ?? NSRect.zero 43 | window?.contentView = hostingView 44 | self.overlayView = overlayView 45 | } 46 | 47 | private func setupNotifications() { 48 | NotificationCenter.default.addObserver( 49 | self, 50 | selector: #selector(showBottomEditView), 51 | name: Notification.Name("showBottomEditView"), 52 | object: nil 53 | ) 54 | 55 | NotificationCenter.default.addObserver( 56 | self, 57 | selector: #selector(hideBottomEditView), 58 | name: Notification.Name("hideBottomEditView"), 59 | object: nil 60 | ) 61 | 62 | NotificationCenter.default.addObserver( 63 | self, 64 | selector: #selector(showToast(_:)), 65 | name: Notification.Name("showToast"), 66 | object: nil 67 | ) 68 | } 69 | 70 | @objc private func showBottomEditView() { 71 | if bottomPanelController == nil { 72 | bottomPanelController = BottomEditPanelController() 73 | } 74 | bottomPanelController?.showWindow(nil) 75 | } 76 | 77 | @objc private func hideBottomEditView() { 78 | bottomPanelController?.hideWindow() 79 | } 80 | 81 | @objc private func showToast(_ notification: Notification) { 82 | guard let message = notification.object as? String else { return } 83 | ToastController.shared.showToast(message: message) 84 | } 85 | 86 | override func showWindow(_ sender: Any?) { 87 | super.showWindow(sender) 88 | AppDelegate.shared.screentId = findCurrentScreenForSwiftUI() 89 | } 90 | 91 | deinit { 92 | NotificationCenter.default.removeObserver(self) 93 | } 94 | } 95 | 96 | // MARK: - Bottom Edit Panel Controller 97 | class BottomEditPanelController: NSWindowController { 98 | convenience init() { 99 | let contentView = NSHostingView(rootView: EditCutBottomView()) 100 | contentView.frame = NSRect(x: 0, y: 0, width: kBottomEditRowWidth, height: kBottomEditRowHeight) 101 | 102 | let window = NSWindow( 103 | contentRect: contentView.frame, 104 | styleMask: [.fullSizeContentView], 105 | backing: .buffered, 106 | defer: false 107 | ) 108 | 109 | window.collectionBehavior = [.canJoinAllSpaces] 110 | window.level = .screenSaver 111 | window.title = kEditImageText 112 | window.contentView = contentView 113 | window.backgroundColor = .clear 114 | window.titleVisibility = .hidden 115 | window.isReleasedWhenClosed = false 116 | window.titlebarAppearsTransparent = true 117 | window.isMovableByWindowBackground = true 118 | 119 | self.init(window: window) 120 | } 121 | 122 | func hideWindow() { 123 | window?.setIsVisible(false) 124 | } 125 | } 126 | 127 | // MARK: - Toast Controller 128 | class ToastController: ObservableObject { 129 | static let shared = ToastController() 130 | 131 | private var toastWindow: NSWindow? 132 | 133 | private init() {} 134 | 135 | func showToast(message: String) { 136 | DispatchQueue.main.async { 137 | self.createToastWindow(message: message) 138 | self.toastWindow?.makeKeyAndOrderFront(nil) 139 | 140 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 141 | self.toastWindow?.close() 142 | self.toastWindow = nil 143 | } 144 | } 145 | } 146 | 147 | private func createToastWindow(message: String) { 148 | let toastHeight: CGFloat = 50 149 | let toastWidth: CGFloat = 300 150 | 151 | let screenSize = NSScreen.main?.frame.size ?? CGSize(width: 800, height: 600) 152 | let toastPosition = CGPoint(x: (screenSize.width - toastWidth) / 2, y: screenSize.height - toastHeight - 100) 153 | 154 | let window = NSWindow( 155 | contentRect: NSRect(x: toastPosition.x, y: toastPosition.y, width: toastWidth, height: toastHeight), 156 | styleMask: [.borderless, .nonactivatingPanel], 157 | backing: .buffered, 158 | defer: false 159 | ) 160 | 161 | window.isOpaque = false 162 | window.backgroundColor = NSColor.black.withAlphaComponent(0.8) 163 | window.level = .screenSaver + 1 164 | window.hasShadow = true 165 | window.isReleasedWhenClosed = false 166 | window.isMovableByWindowBackground = false 167 | 168 | let hostingView = NSHostingView(rootView: SwiftUIToastView(message: message)) 169 | window.contentView = hostingView 170 | 171 | self.toastWindow = window 172 | } 173 | } 174 | 175 | // MARK: - Toast View 176 | struct SwiftUIToastView: View { 177 | let message: String 178 | 179 | var body: some View { 180 | Text(message) 181 | .font(.system(size: 14)) 182 | .foregroundColor(.white) 183 | .frame(maxWidth: .infinity, maxHeight: .infinity) 184 | .background(Color.black.opacity(0.8)) 185 | .cornerRadius(8) 186 | } 187 | } 188 | 189 | // MARK: - Helper Functions 190 | func findCurrentScreenForSwiftUI() -> CGDirectDisplayID? { 191 | let mouseLocation = NSEvent.mouseLocation 192 | let screens = NSScreen.screens 193 | for screen in screens { 194 | let screenFrame = screen.frame 195 | if screenFrame.contains(mouseLocation) { 196 | return screen.displayID 197 | } 198 | } 199 | return nil 200 | } -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Action/VarExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateExtension.swift 3 | // TestMacApp 4 | // 5 | // Created by helinyu on 2024/10/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import FileKit 11 | import ScreenCaptureKit 12 | import Combine 13 | import FileKit 14 | 15 | // 通知的类型 16 | extension Notification.Name { 17 | static let kCutTypeChange = Notification.Name("kCutTypeChange") 18 | static let kSelectColorTypeChange = Notification.Name("kSelectColorTypeChange") 19 | static let kDrawSizeTypeChange = Notification.Name("kDrawSizeTypeChange") 20 | static let kTextSizeTypeChange = Notification.Name("kTextSizeTypeChange") 21 | static let kDownloadClick = Notification.Name("kDownloadClick") 22 | } 23 | 24 | let kplayAudioOfFinished = "playAudioOfFinished" 25 | let ksavePasteboardSameTime = "savePasteboardSameTime" 26 | let konlySaveInPasteBoard = "onlySaveInPasteBoard" 27 | let kautoUpdate = "autoUpdate" 28 | let kautoLaunchByComputer = "autoLaunchByComputer" 29 | let kSelectedSavePath = "kSelectedSavePath1" 30 | 31 | 32 | 33 | class VarExtension { 34 | 35 | static func getTargetName() -> String { 36 | guard let targetName = Bundle.main.infoDictionary?["CFBundleName"] as? String else { 37 | return "" 38 | } 39 | return targetName 40 | } 41 | 42 | @MainActor static func createTargetDirIfNotExit() -> String { 43 | let downLoadPath = Path.userDownloads + self.getTargetName() 44 | self.createDirIfNotExit(downLoadPath.rawValue) 45 | return downLoadPath.rawValue 46 | } 47 | 48 | @MainActor static func createDirIfNotExit(_ atPath: String) { 49 | let fileManager = FileManager.default 50 | var isDirectory: ObjCBool = false 51 | if fileManager.fileExists(atPath: atPath, isDirectory: &isDirectory) { 52 | if !isDirectory.boolValue { 53 | _ = UI.createAlert(title: "Failed to Record", message: "The output path is a file instead of a folder!", button1: "OK").runModal() 54 | return 55 | } 56 | } else { 57 | do { 58 | try fileManager.createDirectory(atPath: atPath, withIntermediateDirectories: true, attributes: nil) 59 | } catch { 60 | _ = UI.createAlert(title: "Failed to Record", message: "Unable to create output folder!", button1: "OK").runModal() 61 | return 62 | } 63 | } 64 | } 65 | 66 | } 67 | 68 | extension Date { 69 | static func getNameByDate() -> String { 70 | let dateFormatter = DateFormatter() 71 | dateFormatter.dateFormat = "y-MM-dd HH.mm.ss" 72 | return dateFormatter.string(from: Date()) 73 | } 74 | } 75 | 76 | 77 | class Auth { 78 | static func requestPermissions() { 79 | DispatchQueue.main.async { 80 | let alert = UI.createAlert(title: "Permission Required", 81 | message: "VCB needs screen recording permissions, even if you only intend on recording audio.", 82 | button1: "Open Settings", 83 | button2: "Quit") 84 | if alert.runModal() == .alertFirstButtonReturn { 85 | NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")!) 86 | } 87 | NSApp.terminate(NSApplication.shared) 88 | } 89 | } 90 | } 91 | 92 | class UI { 93 | @MainActor static func createAlert(title: String, message: String, button1: String, button2: String = "") -> NSAlert { 94 | let alert = NSAlert() 95 | alert.messageText = title 96 | alert.informativeText = message 97 | alert.addButton(withTitle: button1) 98 | if button2 != "" { 99 | alert.addButton(withTitle: button2) 100 | } 101 | alert.alertStyle = .critical 102 | return alert 103 | } 104 | } 105 | 106 | 107 | extension NSPoint { 108 | func isOnEllipse(inRect rect: NSRect) -> Bool { 109 | let centerX = rect.origin.x + rect.width / 2 110 | let centerY = rect.origin.y + rect.height / 2 111 | let radiusX = rect.width / 2 112 | let radiusY = rect.height / 2 113 | let a = pow(self.x - centerX, 2) / pow(radiusX, 2) 114 | let b = pow(self.y - centerY, 2) / pow(radiusY, 2) 115 | return a + b == 1 116 | } 117 | 118 | // 在边线里面 119 | static func isPointOnEllipseInRect(rect: NSRect, pointToCheck: NSPoint) -> Bool { 120 | let centerX = rect.origin.x + rect.size.width / 2 121 | let centerY = rect.origin.y + rect.size.height / 2 122 | let radiusX = rect.size.width / 2 123 | let radiusY = rect.size.height / 2 124 | let dx = pointToCheck.x - centerX 125 | let dy = pointToCheck.y - centerY 126 | let normalizedX = dx / radiusX 127 | let normalizedY = dy / radiusY 128 | return (normalizedX * normalizedX + normalizedY * normalizedY) <= 1 129 | } 130 | 131 | static func isPointOnEllipseBorderInRect(rect: NSRect, pointToCheck: NSPoint, tolerance: CGFloat = 0) -> Bool { 132 | let centerX = rect.origin.x + rect.size.width / 2 133 | let centerY = rect.origin.y + rect.size.height / 2 134 | let radiusX = rect.size.width / 2 135 | let radiusY = rect.size.height / 2 136 | let dx = pointToCheck.x - centerX 137 | let dy = pointToCheck.y - centerY 138 | let normalizedX = dx / radiusX 139 | let normalizedY = dy / radiusY 140 | let distanceFromCenterSquared = normalizedX * normalizedX + normalizedY * normalizedY 141 | return (distanceFromCenterSquared <= (1 + tolerance)) && (distanceFromCenterSquared >= (1 - tolerance)) 142 | } 143 | 144 | static func isPointOnLine(linePoint1: NSPoint, linePoint2: NSPoint, pointToCheck: NSPoint) -> Bool { 145 | let dx = linePoint2.x - linePoint1.x 146 | let dy = linePoint2.y - linePoint1.y 147 | let slope = dy / dx 148 | let yIntercept = linePoint1.y - slope * linePoint1.x 149 | let yAtPointToCheck = slope * pointToCheck.x + yIntercept 150 | return (abs(pointToCheck.y - yAtPointToCheck) <= 5) 151 | } 152 | 153 | static func isPointOnDoodleLine(doodlePoints: [NSPoint], pointToCheck: NSPoint) -> Bool { 154 | if (doodlePoints.count == 0) { return false } 155 | for index in 0.. Bool { 168 | let rect = rect 169 | // 上 170 | if (point.x > rect.minX - deta && point.x < rect.maxX + deta && point.y > rect.maxY - deta && point.y < rect.maxY + deta) { 171 | return true 172 | } 173 | 174 | // 下 175 | if (point.x > rect.minX - deta && point.x < rect.maxX + deta && point.y > rect.minY - deta && point.y < rect.minY + deta) { 176 | return true 177 | } 178 | 179 | // 左 180 | if (point.x > rect.minX - deta && point.x < rect.minX + deta && point.y > rect.minY - deta && point.y < rect.maxY + deta) { 181 | return true 182 | } 183 | // 右 184 | if (point.x > rect.maxX - deta && point.x < rect.maxX + deta && point.y > rect.minY - deta && point.y < rect.maxY + deta) { 185 | return true 186 | } 187 | return false 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/MainOverlay/ScrollCapture/ScreenCaptureHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenCaptureHelper.swift 3 | // ScreenCut 4 | // 5 | // Created by waaw on 21/12/2024. 6 | // 7 | 8 | import Cocoa 9 | import ScreenCaptureKit 10 | 11 | class ScreenCaptureHelper: NSObject, SCStreamDelegate, SCStreamOutput { 12 | 13 | private var captureStream: SCStream? 14 | private var capturedImages: [NSImage] = [] 15 | private var scrollHeight: CGFloat = 0 16 | private var screenWidth: CGFloat = 0 17 | private var scrollPosition: CGFloat = 0 18 | private var screenHeight: CGFloat = 0 19 | 20 | func startCapturing(scrollHeight: CGFloat, screenWidth: CGFloat, screenHeight: CGFloat) { 21 | self.scrollHeight = scrollHeight 22 | self.screenWidth = screenWidth 23 | self.screenHeight = screenHeight 24 | self.scrollPosition = 0 25 | self.capturedImages.removeAll() 26 | 27 | captureScreen() 28 | } 29 | 30 | 31 | func captureScreen() { 32 | checkScreenRecordingPermission() 33 | 34 | let content = ScreenCut.availableContent 35 | guard let displays = content?.displays, let display = displays.first else { 36 | print("No available displays") 37 | return 38 | } 39 | 40 | let contentFilter = SCContentFilter(display: display, excludingWindows: []) 41 | let config = SCStreamConfiguration() 42 | config.width = display.width 43 | config.height = display.height 44 | config.pixelFormat = kCVPixelFormatType_32BGRA 45 | config.minimumFrameInterval = CMTimeMake(value: 1, timescale: 30) // 30 FPS 46 | 47 | do { 48 | captureStream = SCStream(filter: contentFilter, configuration: config, delegate: self) 49 | print("Capture stream initialized successfully") 50 | } catch { 51 | print("Failed to initialize capture stream: \(error.localizedDescription)") 52 | } 53 | } 54 | 55 | 56 | func checkScreenRecordingPermission() { 57 | let isTrusted = CGPreflightScreenCaptureAccess() 58 | if !isTrusted { 59 | print("Screen recording permission is not granted. Please enable it in System Preferences.") 60 | } 61 | } 62 | 63 | 64 | // private func captureScreen() { 65 | // let content = ScreenCut.availableContent 66 | // guard let displays = content?.displays, let display = displays.first else { 67 | // print("No available displays") 68 | // return 69 | // } 70 | // let contentFilter = SCContentFilter(display: display, excludingWindows: []) 71 | // 72 | // let configuration = SCStreamConfiguration() 73 | // 74 | // configuration.width = display.width 75 | // configuration.height = display.height 76 | // 77 | // configuration.pixelFormat = kCVPixelFormatType_32BGRA 78 | // configuration.minimumFrameInterval = CMTimeMake(value: 1, timescale: 30) // 设置帧率 79 | // 80 | // captureStream = SCStream(filter: contentFilter, configuration: configuration, delegate: self) 81 | // 82 | // do { 83 | // let sampleHandlerQueue = DispatchQueue(label: "com.example.videoFrameHandlerQueue") 84 | //// try captureStream?.addStreamOutput(self, type: .screen, sampleHandlerQueue: sampleHandlerQueue) 85 | //// DispatchQueue.main.async { 86 | //// self.captureStream?.startCapture() 87 | //// print("Started screen capture") 88 | //// } 89 | // } catch { 90 | // print("Failed to start screen capture: \(error.localizedDescription)") 91 | // } 92 | // } 93 | 94 | // MARK: - SCStreamDelegate 95 | func stream(_ stream: SCStream, didStopWithError error: any Error) { 96 | print("lt -- didStopWithError") 97 | } 98 | 99 | /** 100 | @abstract outputVideoEffectDidStartForStream: 101 | @param stream the SCStream object 102 | @discussion notifies the delegate that the stream's overlay video effect has started. 103 | */ 104 | @available(macOS 14.0, *) 105 | func outputVideoEffectDidStart(for stream: SCStream) { 106 | print("lt -- outputVideoEffectDidStart") 107 | } 108 | 109 | /** 110 | @abstract stream:outputVideoEffectDidStart: 111 | @param stream the SCStream object 112 | @discussion notifies the delegate that the stream's overlay video effect has stopped. 113 | */ 114 | func outputVideoEffectDidStop(for stream: SCStream) { 115 | print("lt -- outputVideoEffectDidStop") 116 | } 117 | 118 | // protocol 119 | func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) { 120 | print("lt -- didOutputSampleBuffer") 121 | // 将 sampleBuffer 转换为 NSImage 122 | if let capturedImageRef = imageFromSampleBuffer(sampleBuffer) { 123 | let capturedImage = NSImage(cgImage: capturedImageRef, size: .zero) 124 | capturedImages.append(capturedImage) 125 | } 126 | 127 | scrollPosition += scrollHeight 128 | if scrollPosition < screenHeight { 129 | scrollDown() 130 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 131 | self.captureScreen() 132 | } 133 | } else { 134 | createFinalImage() 135 | } 136 | } 137 | 138 | private func imageFromSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> CGImage? { 139 | // 从 CMSampleBuffer 中提取出图像 140 | guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil } 141 | let ciImage = CIImage(cvImageBuffer: imageBuffer) 142 | let context = CIContext() 143 | return context.createCGImage(ciImage, from: ciImage.extent) 144 | } 145 | 146 | private func scrollDown() { 147 | // 使用 CGEvent 模拟滚动 148 | if let scrollEvent = CGEvent.init( 149 | scrollWheelEvent2Source: nil, 150 | units: .line, 151 | wheelCount: 1, 152 | wheel1: -2, 153 | wheel2: 0, 154 | wheel3: 0 155 | ) { 156 | scrollEvent.post(tap: .cghidEventTap) 157 | } 158 | } 159 | 160 | private func createFinalImage() { 161 | guard let finalImage = concatenateImages(capturedImages) else { return } 162 | saveImage(finalImage) 163 | } 164 | 165 | private func concatenateImages(_ images: [NSImage]) -> NSImage? { 166 | guard !images.isEmpty else { return nil } 167 | 168 | let totalHeight = images.reduce(0) { $0 + $1.size.height } 169 | let width = images.first?.size.width ?? 0 170 | 171 | let finalImage = NSImage(size: NSSize(width: width, height: totalHeight)) 172 | finalImage.lockFocus() 173 | 174 | var yOffset: CGFloat = 0 175 | for image in images { 176 | image.draw(in: NSRect(x: 0, y: yOffset, width: width, height: image.size.height)) 177 | yOffset += image.size.height 178 | } 179 | 180 | finalImage.unlockFocus() 181 | return finalImage 182 | } 183 | 184 | private func saveImage(_ image: NSImage) { 185 | guard let imageData = image.tiffRepresentation, 186 | let bitmapRep = NSBitmapImageRep(data: imageData) else { return } 187 | 188 | let pngData = bitmapRep.representation(using: .png, properties: [:]) 189 | 190 | let savePanel = NSSavePanel() 191 | savePanel.allowedFileTypes = ["png"] 192 | 193 | savePanel.begin { response in 194 | if response == .OK, let url = savePanel.url { 195 | do { 196 | try pngData?.write(to: url, options: .atomic) 197 | print("Image saved to \(url)") 198 | } catch { 199 | print("Error saving file: \(error.localizedDescription)") 200 | } 201 | } 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/HelpView/PreferenceSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/10/26. 6 | // 7 | 8 | import SwiftUI 9 | import KeyboardShortcuts 10 | import Sparkle 11 | import ServiceManagement 12 | 13 | let kLeftTextWidth = 120.0 14 | let kRightFirstSpaceWidth = 20.0 15 | let kDesktoptext = "桌面" 16 | let kDocumentText = "文档" 17 | let kImageText = "图片" 18 | let kOtherText = "其他" 19 | let kDefaultText = "ScreenCut" 20 | 21 | 22 | struct PreferenceSettingsView: View { 23 | 24 | enum PathSelectionThpe: String, CaseIterable { 25 | case defaultS, desktopS, documentS, imageS 26 | var id: Self { self } 27 | 28 | var path: String { 29 | switch self { 30 | case .defaultS: 31 | return defaultSavepath 32 | case .desktopS: 33 | return FileManager.default.urls(for:.desktopDirectory, in:.userDomainMask).first?.path ?? "" 34 | case .documentS: 35 | return FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first?.path ?? "" 36 | case .imageS: 37 | return FileManager.default.urls(for:.picturesDirectory, in:.userDomainMask).first?.path ?? "" 38 | } 39 | } 40 | 41 | var name: String { 42 | switch self { 43 | case .defaultS: 44 | return "ScreenCut" 45 | case .desktopS: 46 | return kDesktoptext 47 | case .documentS: 48 | return kDocumentText 49 | case .imageS: 50 | return kImageText 51 | } 52 | } 53 | } 54 | 55 | @AppStorage(kplayAudioOfFinished) private var playAudioOfFinished: Bool = false // 截图完成播放音效 56 | @AppStorage(ksavePasteboardSameTime) private var savePasteboardSameTime: Bool = true // 同时拷贝到粘贴版 57 | @AppStorage(konlySaveInPasteBoard) private var onlySaveInPasteBoard: Bool = false // 只是拷贝到粘地板 58 | @AppStorage(kautoUpdate) private var autoUpdate: Bool = false // 自动更新 59 | @AppStorage(kautoLaunchByComputer) private var autoLaunchByComputer: Bool = false // 根据电脑自动启动 60 | @AppStorage(kSelectedSavePath) private var lastSelectedPath: String = defaultSavepath 61 | 62 | @State private var selectOption:PathSelectionThpe = .defaultS 63 | 64 | var body: some View { 65 | VStack(alignment: .leading) { 66 | HStack(alignment: .center) { 67 | Spacer().frame(width: 20) 68 | Image("logo-img-white") 69 | VStack(alignment: .leading) { 70 | Text("偏好设置") 71 | .fontWeight(.bold) 72 | .font(.title) 73 | Text("请使用前完成一下设置") 74 | .font(.system(size: 14)) 75 | } 76 | Spacer() 77 | } 78 | .frame(height: 80.0) 79 | .background(.black) 80 | .foregroundColor(.white) 81 | VStack (alignment: .leading){ 82 | HStack() { 83 | Text("全屏截图快捷键: ") 84 | .frame(width: kLeftTextWidth, alignment: .trailing) 85 | KeyboardShortcuts.Recorder("", name: .fullScreenCut) 86 | } 87 | HStack() { 88 | Text("区域截图快捷键: ") 89 | .frame(width: kLeftTextWidth, alignment: .trailing) 90 | KeyboardShortcuts.Recorder("", name: .selectedAreaCut) 91 | } 92 | HStack(alignment: .top) { 93 | Text("截屏时: ") 94 | .frame(width: kLeftTextWidth, alignment: .trailing) 95 | VStack(alignment: .leading) { 96 | HStack { 97 | Spacer().frame(width: kRightFirstSpaceWidth) 98 | Toggle("截图完成后播放声音", isOn: $playAudioOfFinished) 99 | .toggleStyle(CheckboxToggleStyle()) 100 | } 101 | // 滚动截屏 102 | // HStack { 103 | // Spacer().frame(width: kRightFirstSpaceWidth) 104 | // Toggle("启动滚动截屏", isOn: $playAudioOfFinished) 105 | // .toggleStyle(CheckboxToggleStyle()) 106 | // } 107 | } 108 | } 109 | // Divider() 110 | // Spacer().frame(height: 10.0) 111 | // HStack { 112 | // Text("图片大小: ") 113 | // .frame(width: kLeftTextWidth, alignment: .trailing) 114 | // .background(.purple) 115 | // HStack { 116 | // Spacer().frame(width: kRightFirstSpaceWidth) 117 | // Toggle("高清屏幕(Retina)下载取1x大小图片", isOn: $playAudioOfFinished) 118 | // .toggleStyle(CheckboxToggleStyle()) 119 | // } 120 | // } 121 | // Spacer().frame(height: 10.0) 122 | Divider() 123 | Spacer().frame(height: 10.0) 124 | HStack(alignment: .top) { 125 | Text("图片保存的位置:") 126 | .frame(width: kLeftTextWidth, alignment: .trailing) 127 | VStack(alignment: .leading) { 128 | HStack { 129 | Text(self.lastSelectedPath) 130 | Button("修改") { 131 | let openPanel = NSOpenPanel() 132 | openPanel.canChooseFiles = false 133 | openPanel.canChooseDirectories = true 134 | openPanel.allowedContentTypes = [] 135 | openPanel.allowsOtherFileTypes = false 136 | if openPanel.runModal() == NSApplication.ModalResponse.OK { 137 | if let path = openPanel.urls.first?.path { 138 | self.lastSelectedPath = path 139 | } 140 | } 141 | } 142 | } 143 | Toggle("同时保存在粘贴版", isOn: $savePasteboardSameTime) 144 | .toggleStyle(CheckboxToggleStyle()) 145 | Toggle("只保存到粘贴版", isOn: $onlySaveInPasteBoard) 146 | .toggleStyle(CheckboxToggleStyle()) 147 | } 148 | } 149 | Spacer().frame(height: 10.0) 150 | Divider() 151 | Spacer().frame(height: 10.0) 152 | HStack(alignment: .top) { 153 | VStack(alignment: .leading) { 154 | HStack { 155 | Spacer().frame(width: kLeftTextWidth) 156 | Spacer().frame(width: kRightFirstSpaceWidth) 157 | Toggle("开机自动启动", isOn: $autoLaunchByComputer) 158 | .toggleStyle(CheckboxToggleStyle()) 159 | } 160 | .onChange(of: autoLaunchByComputer) { oldValue, newValue in 161 | print("lt -- new value : \(newValue)") 162 | // 切换主应用启动设置 163 | do { 164 | if newValue { 165 | try SMAppService.mainApp.register() 166 | } else { 167 | try SMAppService.mainApp.unregister() 168 | } 169 | } catch { 170 | print("Failed to update launch at login setting: \(error)") 171 | } 172 | } 173 | HStack { 174 | Spacer().frame(width: kLeftTextWidth) 175 | Spacer().frame(width: kRightFirstSpaceWidth) 176 | Toggle("自动检查更新", isOn: $autoUpdate) 177 | .toggleStyle(CheckboxToggleStyle()) 178 | } 179 | } 180 | } 181 | HStack { 182 | Spacer().frame(width: kLeftTextWidth) 183 | Spacer().frame(width: kRightFirstSpaceWidth) 184 | Button { 185 | NotificationCenter.default.post(name: Notification.Name("update.app.noti"), object: "") 186 | } label: { 187 | Text("检查更新") 188 | } 189 | } 190 | 191 | Spacer().frame(height: 30.0) 192 | }.padding(EdgeInsets(top: 0, leading: 40, bottom: 0, trailing: 0)) 193 | } 194 | } 195 | } 196 | 197 | 198 | class PreferenceSettingsViewController: NSWindowController { 199 | 200 | convenience init() { 201 | let window = NSWindow( 202 | contentRect: NSRect(x: 0, y: 0, width: 560, height: 500), 203 | styleMask: [.titled, .closable], 204 | backing: .buffered, 205 | defer: false 206 | ) 207 | 208 | window.center() 209 | window.setFrameAutosaveName("偏好设置") 210 | window.level = .normal + 1 211 | window.contentView = NSHostingView(rootView: PreferenceSettingsView()) 212 | 213 | self.init(window: window) 214 | } 215 | 216 | override func showWindow(_ sender: Any?) { 217 | super.showWindow(sender) 218 | } 219 | } 220 | 221 | 222 | #Preview { 223 | PreferenceSettingsView() 224 | } 225 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/SwiftUIComponents/PreferenceSettingsWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreferenceSettingsWindowController.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/12/19. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | import KeyboardShortcuts 11 | import Sparkle 12 | import ServiceManagement 13 | 14 | struct SwiftUIPreferenceSettingsView: View { 15 | 16 | enum PathSelectionType: String, CaseIterable { 17 | case defaultS, desktopS, documentS, imageS 18 | var id: Self { self } 19 | 20 | var path: String { 21 | switch self { 22 | case .defaultS: 23 | return defaultSavepath 24 | case .desktopS: 25 | return FileManager.default.urls(for:.desktopDirectory, in:.userDomainMask).first?.path ?? "" 26 | case .documentS: 27 | return FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first?.path ?? "" 28 | case .imageS: 29 | return FileManager.default.urls(for:.picturesDirectory, in:.userDomainMask).first?.path ?? "" 30 | } 31 | } 32 | 33 | var name: String { 34 | switch self { 35 | case .defaultS: 36 | return "ScreenCut" 37 | case .desktopS: 38 | return kDesktoptext 39 | case .documentS: 40 | return kDocumentText 41 | case .imageS: 42 | return kImageText 43 | } 44 | } 45 | } 46 | 47 | @AppStorage(kplayAudioOfFinished) private var playAudioOfFinished: Bool = false 48 | @AppStorage(ksavePasteboardSameTime) private var savePasteboardSameTime: Bool = true 49 | @AppStorage(konlySaveInPasteBoard) private var onlySaveInPasteBoard: Bool = false 50 | @AppStorage(kautoUpdate) private var autoUpdate: Bool = false 51 | @AppStorage(kautoLaunchByComputer) private var autoLaunchByComputer: Bool = false 52 | @AppStorage(kSelectedSavePath) private var lastSelectedPath: String = defaultSavepath 53 | 54 | @State private var selectOption: PathSelectionType = .defaultS 55 | 56 | var body: some View { 57 | VStack(alignment: .leading, spacing: 0) { 58 | // Header 59 | HStack(alignment: .center) { 60 | Spacer().frame(width: 20) 61 | Image("logo-img-white") 62 | .resizable() 63 | .frame(width: 60, height: 60) 64 | 65 | VStack(alignment: .leading, spacing: 4) { 66 | Text("偏好设置") 67 | .font(.title) 68 | .fontWeight(.bold) 69 | .foregroundColor(.white) 70 | 71 | Text("请使用前完成一下设置") 72 | .font(.system(size: 14)) 73 | .foregroundColor(.white) 74 | } 75 | 76 | Spacer() 77 | } 78 | .frame(height: 80) 79 | .frame(maxWidth: .infinity) 80 | .background(Color.black) 81 | 82 | // Content 83 | ScrollView { 84 | VStack(alignment: .leading, spacing: 20) { 85 | // Keyboard Shortcuts 86 | VStack(alignment: .leading, spacing: 12) { 87 | HStack { 88 | Text("全屏截图快捷键: ") 89 | .frame(width: kLeftTextWidth, alignment: .trailing) 90 | KeyboardShortcuts.Recorder("", name: .fullScreenCut) 91 | } 92 | 93 | HStack { 94 | Text("区域截图快捷键: ") 95 | .frame(width: kLeftTextWidth, alignment: .trailing) 96 | KeyboardShortcuts.Recorder("", name: .selectedAreaCut) 97 | } 98 | } 99 | 100 | Divider() 101 | 102 | // Screenshot Settings 103 | VStack(alignment: .leading, spacing: 12) { 104 | HStack(alignment: .top) { 105 | Text("截屏时: ") 106 | .frame(width: kLeftTextWidth, alignment: .trailing) 107 | 108 | VStack(alignment: .leading, spacing: 8) { 109 | HStack { 110 | Spacer().frame(width: kRightFirstSpaceWidth) 111 | Toggle("截图完成后播放声音", isOn: $playAudioOfFinished) 112 | .toggleStyle(CheckboxToggleStyle()) 113 | } 114 | } 115 | } 116 | } 117 | 118 | Divider() 119 | 120 | // Save Settings 121 | VStack(alignment: .leading, spacing: 12) { 122 | HStack(alignment: .top) { 123 | Text("图片保存的位置:") 124 | .frame(width: kLeftTextWidth, alignment: .trailing) 125 | 126 | VStack(alignment: .leading, spacing: 8) { 127 | HStack { 128 | Text(self.lastSelectedPath) 129 | .font(.system(size: 12)) 130 | .foregroundColor(.secondary) 131 | 132 | Button("修改") { 133 | let openPanel = NSOpenPanel() 134 | openPanel.canChooseFiles = false 135 | openPanel.canChooseDirectories = true 136 | openPanel.allowedContentTypes = [] 137 | openPanel.allowsOtherFileTypes = false 138 | 139 | if openPanel.runModal() == NSApplication.ModalResponse.OK { 140 | if let path = openPanel.urls.first?.path { 141 | self.lastSelectedPath = path 142 | } 143 | } 144 | } 145 | .buttonStyle(.bordered) 146 | } 147 | 148 | Toggle("同时保存在粘贴版", isOn: $savePasteboardSameTime) 149 | .toggleStyle(CheckboxToggleStyle()) 150 | 151 | Toggle("只保存到粘贴版", isOn: $onlySaveInPasteBoard) 152 | .toggleStyle(CheckboxToggleStyle()) 153 | } 154 | } 155 | } 156 | 157 | Divider() 158 | 159 | // App Settings 160 | VStack(alignment: .leading, spacing: 12) { 161 | HStack { 162 | Spacer().frame(width: kLeftTextWidth) 163 | Spacer().frame(width: kRightFirstSpaceWidth) 164 | 165 | VStack(alignment: .leading, spacing: 8) { 166 | Toggle("开机自动启动", isOn: $autoLaunchByComputer) 167 | .toggleStyle(CheckboxToggleStyle()) 168 | .onChange(of: autoLaunchByComputer) { oldValue, newValue in 169 | do { 170 | if newValue { 171 | try SMAppService.mainApp.register() 172 | } else { 173 | try SMAppService.mainApp.unregister() 174 | } 175 | } catch { 176 | print("Failed to update launch at login setting: \(error)") 177 | } 178 | } 179 | 180 | Toggle("自动检查更新", isOn: $autoUpdate) 181 | .toggleStyle(CheckboxToggleStyle()) 182 | 183 | Button { 184 | NotificationCenter.default.post(name: Notification.Name("update.app.noti"), object: "") 185 | } label: { 186 | Text("检查更新") 187 | } 188 | .buttonStyle(.bordered) 189 | } 190 | } 191 | } 192 | 193 | Spacer().frame(height: 30) 194 | } 195 | .padding(.horizontal, 40) 196 | .padding(.vertical, 20) 197 | } 198 | } 199 | .frame(width: 560, height: 500) 200 | .background(Color.white) 201 | } 202 | } 203 | 204 | class PreferenceSettingsWindowController: NSWindowController { 205 | 206 | convenience init() { 207 | let window = NSWindow( 208 | contentRect: NSRect(x: 0, y: 0, width: 560, height: 500), 209 | styleMask: [.titled, .closable], 210 | backing: .buffered, 211 | defer: false 212 | ) 213 | 214 | window.center() 215 | window.setFrameAutosaveName("偏好设置") 216 | window.level = .normal + 1 217 | window.contentView = NSHostingView(rootView: SwiftUIPreferenceSettingsView()) 218 | 219 | self.init(window: window) 220 | } 221 | 222 | override func showWindow(_ sender: Any?) { 223 | super.showWindow(sender) 224 | } 225 | } 226 | 227 | #Preview { 228 | PreferenceSettingsView() 229 | } -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/MainOverlay/showView/ScreenshotCircleView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import ScreenCaptureKit 4 | import AppKit 5 | 6 | //椭圆形 7 | class ScreenshotCircleView: ScreenshotBaseOverlayView { 8 | 9 | var selectionRect: NSRect = NSRect.zero 10 | var initialLocation: NSPoint? 11 | var dragIng: Bool = false 12 | var activeHandle: RetangleResizeHandle = .none 13 | var lastMouseLocation: NSPoint? 14 | var maxFrame: NSRect? 15 | let controlPointDiameter: CGFloat = 8.0 16 | let controlPointColor: NSColor = NSColor.white 17 | var fillOverLayeralpha: CGFloat = 0.0 // 默认值 18 | 19 | var hasSelectionRect: Bool { 20 | return (self.selectionRect.size.width > 0 && self.selectionRect.size.height > 0) 21 | } 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | override func viewDidMoveToWindow() { 32 | super.viewDidMoveToWindow() 33 | 34 | // let trackingArea = NSTrackingArea(rect: self.bounds, 35 | // options: [.mouseEnteredAndExited, .mouseMoved, .cursorUpdate, .activeInActiveApp], 36 | // owner: self, 37 | // userInfo: nil) 38 | // self.addTrackingArea(trackingArea) 39 | 40 | // selectionRect = NSRect(x: (self.frame.width - size.width) / 2, y: (self.frame.height - size.height) / 2, width: size.width, height:size.height) 41 | } 42 | 43 | override func draw(_ dirtyRect: NSRect) { 44 | super.draw(dirtyRect) 45 | maxFrame = dirtyRect 46 | 47 | if !self.hasSelectionRect { 48 | return 49 | } 50 | 51 | NSColor.red.withAlphaComponent(self.fillOverLayeralpha).setFill() 52 | dirtyRect.fill() 53 | 54 | let rect = self.selectionRect // 绘制椭圆 55 | let path = NSBezierPath(ovalIn: rect) 56 | path.fill() 57 | selectedColor.setStroke() 58 | path.lineWidth = lineWidth 59 | path.stroke() 60 | 61 | // 绘制边框中的点 62 | if (!editFinished) { 63 | for handle in RetangleResizeHandle.allCases { 64 | if let point = controlPointForHandle(handle, inRect: rect) { 65 | let controlPointRect = NSRect(origin: point, size: CGSize(width: controlPointDiameter, height: controlPointDiameter)) 66 | let controlPointPath = NSBezierPath(ovalIn: controlPointRect) 67 | controlPointColor.setFill() 68 | controlPointPath.fill() 69 | } 70 | } 71 | } 72 | } 73 | 74 | override func isOnBorderAt(_ point: NSPoint) -> Bool { 75 | // print("lt -- circle point:\(point) selectionRect:\(self.selectionRect)") 76 | // return point.isOnEllipse(inRect: self.selectionRect) 77 | return NSPoint.isPointOnEllipseBorderInRect(rect: self.selectionRect, pointToCheck: point, tolerance: 0.1) 78 | } 79 | 80 | override func handleForPoint(_ point: NSPoint) -> RetangleResizeHandle { 81 | if !self.hasSelectionRect { return .none } 82 | let rect = selectionRect 83 | for handle in RetangleResizeHandle.allCases { 84 | if let controlPoint = controlPointForHandle(handle, inRect: rect), NSRect(origin: controlPoint, size: CGSize(width: controlPointDiameter, height: controlPointDiameter)).contains(point) { 85 | return handle 86 | } 87 | } 88 | return .none 89 | } 90 | 91 | func controlPointForHandle(_ handle: RetangleResizeHandle, inRect rect: NSRect) -> NSPoint? { 92 | switch handle { 93 | case .topLeft: 94 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.maxY - controlPointDiameter / 2 + 1) 95 | case .top: 96 | return NSPoint(x: rect.midX - controlPointDiameter / 2, y: rect.maxY - controlPointDiameter / 2 + 1) 97 | case .topRight: 98 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.maxY - controlPointDiameter / 2 + 1) 99 | case .right: 100 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.midY - controlPointDiameter / 2) 101 | case .bottomRight: 102 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.minY - controlPointDiameter / 2 - 1) 103 | case .bottom: 104 | return NSPoint(x: rect.midX - controlPointDiameter / 2, y: rect.minY - controlPointDiameter / 2 - 1) 105 | case .bottomLeft: 106 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.minY - controlPointDiameter / 2 - 1) 107 | case .left: 108 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.midY - controlPointDiameter / 2) 109 | case .none: 110 | return nil 111 | } 112 | } 113 | 114 | override func mouseDragged(with event: NSEvent) { 115 | print("lt -- circle mouse drag") 116 | guard var initialLocation = initialLocation else { return } 117 | let currentLocation = convert(event.locationInWindow, from: nil) 118 | 119 | if activeHandle != .none { 120 | var newRect = selectionRect 121 | let lastLocation = lastMouseLocation ?? currentLocation 122 | 123 | let deltaX = currentLocation.x - lastLocation.x 124 | let deltaY = currentLocation.y - lastLocation.y 125 | 126 | switch activeHandle { 127 | case .topLeft: 128 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 129 | newRect.size.width = max(20, newRect.size.width - deltaX) 130 | newRect.size.height = max(20, newRect.size.height + deltaY) 131 | case .top: 132 | newRect.size.height = max(20, newRect.size.height + deltaY) 133 | case .topRight: 134 | newRect.size.width = max(20, newRect.size.width + deltaX) 135 | newRect.size.height = max(20, newRect.size.height + deltaY) 136 | case .right: 137 | newRect.size.width = max(20, newRect.size.width + deltaX) 138 | case .bottomRight: 139 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 140 | newRect.size.width = max(20, newRect.size.width + deltaX) 141 | newRect.size.height = max(20, newRect.size.height - deltaY) 142 | case .bottom: 143 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 144 | newRect.size.height = max(20, newRect.size.height - deltaY) 145 | case .bottomLeft: 146 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 147 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 148 | newRect.size.width = max(20, newRect.size.width - deltaX) 149 | newRect.size.height = max(20, newRect.size.height - deltaY) 150 | case .left: 151 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 152 | newRect.size.width = max(20, newRect.size.width - deltaX) 153 | default: 154 | break 155 | } 156 | self.selectionRect = newRect 157 | initialLocation = currentLocation // Update initial location for continuous dragging 158 | } else { 159 | if dragIng { 160 | dragIng = true 161 | // 计算移动偏移量 162 | let deltaX = currentLocation.x - initialLocation.x 163 | let deltaY = currentLocation.y - initialLocation.y 164 | 165 | // 更新矩形位置 166 | let x = self.selectionRect.origin.x 167 | let y = self.selectionRect.origin.y 168 | let w = self.selectionRect.size.width 169 | let h = self.selectionRect.size.height 170 | self.selectionRect.origin.x = min(max(0.0, x + deltaX), self.frame.width - w) 171 | self.selectionRect.origin.y = min(max(0.0, y + deltaY), self.frame.height - h) 172 | initialLocation = currentLocation 173 | } else { 174 | // 创建新矩形 175 | guard let maxFrame = maxFrame else { return } 176 | let origin = NSPoint(x: max(maxFrame.origin.x, min(initialLocation.x, currentLocation.x)), y: max(maxFrame.origin.y, min(initialLocation.y, currentLocation.y))) 177 | var maxH = abs(currentLocation.y - initialLocation.y) 178 | var maxW = abs(currentLocation.x - initialLocation.x) 179 | if currentLocation.y < maxFrame.origin.y { maxH = initialLocation.y } 180 | if currentLocation.x < maxFrame.origin.x { maxW = initialLocation.x } 181 | let size = NSSize(width: maxW, height: maxH) 182 | self.selectionRect = NSIntersectionRect(maxFrame, NSRect(origin: origin, size: size)) 183 | } 184 | self.initialLocation = initialLocation 185 | } 186 | lastMouseLocation = currentLocation 187 | needsDisplay = true 188 | } 189 | 190 | override func mouseDown(with event: NSEvent) { 191 | let location = convert(event.locationInWindow, from: nil) 192 | initialLocation = location 193 | lastMouseLocation = location 194 | activeHandle = handleForPoint(location) 195 | if NSPointInRect(location, self.selectionRect) { 196 | dragIng = true 197 | } 198 | needsDisplay = true 199 | } 200 | 201 | override func mouseUp(with event: NSEvent) { 202 | initialLocation = nil 203 | activeHandle = .none 204 | dragIng = false 205 | needsDisplay = true 206 | } 207 | 208 | override func hitTest(_ point: NSPoint) -> NSView? { 209 | let hitView = super.hitTest(point) 210 | if hitView == self { 211 | return self.superview 212 | } 213 | return hitView 214 | } 215 | } 216 | 217 | 218 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/MainOverlay/showView/ScreenshotArrowView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import ScreenCaptureKit 4 | import AppKit 5 | 6 | //椭圆形 7 | class ScreenshotArrowView: ScreenshotBaseOverlayView { 8 | 9 | var selectionRect: NSRect = NSRect.zero 10 | var initialLocation: NSPoint? 11 | var dragIng: Bool = false 12 | var activeHandle: RetangleResizeHandle = .none 13 | var lastMouseLocation: NSPoint? 14 | var maxFrame: NSRect? 15 | let controlPointDiameter: CGFloat = 8.0 16 | let controlPointColor: NSColor = NSColor.white 17 | var fillOverLayeralpha: CGFloat = 0.0 // 默认值 18 | // var selectedColor: NSColor = NSColor.white 19 | // var lineWidth: CGFloat = 2.0 20 | var hasSelectionRect: Bool { 21 | return (self.selectionRect.size.width > 0 && self.selectionRect.size.height > 0) 22 | } 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | } 27 | 28 | required init?(coder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | override func viewDidMoveToWindow() { 33 | super.viewDidMoveToWindow() 34 | } 35 | 36 | override func draw(_ dirtyRect: NSRect) { 37 | super.draw(dirtyRect) 38 | maxFrame = dirtyRect 39 | 40 | NSColor.red.withAlphaComponent(fillOverLayeralpha).setFill() 41 | dirtyRect.fill() 42 | 43 | if (!self.hasSelectionRect) { 44 | return 45 | } 46 | 47 | let rect = selectionRect 48 | let mousePoint: NSPoint = lastMouseLocation! 49 | 50 | // 设置箭头的颜色 51 | NSColor.clear.setFill() 52 | selectedColor.setStroke() 53 | 54 | // 创建箭头路径 55 | let arrowPath = NSBezierPath() 56 | 57 | // 箭头的起点 58 | let arrowStart = initialLocation! 59 | arrowPath.move(to: arrowStart) 60 | 61 | // 箭头的主干 62 | arrowPath.line(to: mousePoint) 63 | 64 | // 箭头的尖端 65 | let arrowLength: CGFloat = 20.0 66 | let angle: CGFloat = atan2(mousePoint.y - arrowStart.y, mousePoint.x - arrowStart.x) 67 | 68 | let arrowHead1 = NSPoint(x: mousePoint.x - arrowLength * cos(angle - .pi / 6), 69 | y: mousePoint.y - arrowLength * sin(angle - .pi / 6)) 70 | let arrowHead2 = NSPoint(x: mousePoint.x - arrowLength * cos(angle + .pi / 6), 71 | y: mousePoint.y - arrowLength * sin(angle + .pi / 6)) 72 | 73 | arrowPath.line(to: arrowHead1) 74 | arrowPath.move(to: mousePoint) 75 | arrowPath.line(to: arrowHead2) 76 | 77 | // 完成路径 78 | arrowPath.lineWidth = lineWidth 79 | arrowPath.stroke() 80 | 81 | // 绘制边框中的点 82 | if (!editFinished) { 83 | for handle in RetangleResizeHandle.allCases { 84 | if let point = controlPointForHandle(handle, inRect: rect) { 85 | let controlPointRect = NSRect(origin: point, size: CGSize(width: controlPointDiameter, height: controlPointDiameter)) 86 | let controlPointPath = NSBezierPath(ovalIn: controlPointRect) 87 | controlPointColor.setFill() 88 | controlPointPath.fill() 89 | } 90 | } 91 | } 92 | } 93 | 94 | override func isOnBorderAt(_ point: NSPoint) -> Bool { 95 | guard let line1 = initialLocation, let line2 = lastMouseLocation else { 96 | print("线条点没有获取到") 97 | return false 98 | } 99 | let flag = NSPoint.isPointOnLine(linePoint1: line1, linePoint2: line2, pointToCheck: point) 100 | return flag 101 | } 102 | 103 | override func handleForPoint(_ point: NSPoint) -> RetangleResizeHandle { 104 | if !self.hasSelectionRect { 105 | return .none 106 | } 107 | let rect = self.selectionRect 108 | for handle in RetangleResizeHandle.allCases { 109 | if let controlPoint = controlPointForHandle(handle, inRect: rect), NSRect(origin: controlPoint, size: CGSize(width: controlPointDiameter, height: controlPointDiameter)).contains(point) { 110 | return handle 111 | } 112 | } 113 | return .none 114 | } 115 | 116 | func controlPointForHandle(_ handle: RetangleResizeHandle, inRect rect: NSRect) -> NSPoint? { 117 | switch handle { 118 | case .topLeft: 119 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.maxY - controlPointDiameter / 2 + 1) 120 | case .top: 121 | return NSPoint(x: rect.midX - controlPointDiameter / 2, y: rect.maxY - controlPointDiameter / 2 + 1) 122 | case .topRight: 123 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.maxY - controlPointDiameter / 2 + 1) 124 | case .right: 125 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.midY - controlPointDiameter / 2) 126 | case .bottomRight: 127 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.minY - controlPointDiameter / 2 - 1) 128 | case .bottom: 129 | return NSPoint(x: rect.midX - controlPointDiameter / 2, y: rect.minY - controlPointDiameter / 2 - 1) 130 | case .bottomLeft: 131 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.minY - controlPointDiameter / 2 - 1) 132 | case .left: 133 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.midY - controlPointDiameter / 2) 134 | case .none: 135 | return nil 136 | } 137 | } 138 | 139 | override func mouseDragged(with event: NSEvent) { 140 | guard var initialLocation = initialLocation else { return } 141 | let currentLocation = convert(event.locationInWindow, from: nil) 142 | 143 | if activeHandle != .none { 144 | var newRect = selectionRect 145 | let lastLocation = lastMouseLocation ?? currentLocation 146 | 147 | let deltaX = currentLocation.x - lastLocation.x 148 | let deltaY = currentLocation.y - lastLocation.y 149 | 150 | switch activeHandle { 151 | case .topLeft: 152 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 153 | newRect.size.width = max(20, newRect.size.width - deltaX) 154 | newRect.size.height = max(20, newRect.size.height + deltaY) 155 | case .top: 156 | newRect.size.height = max(20, newRect.size.height + deltaY) 157 | case .topRight: 158 | newRect.size.width = max(20, newRect.size.width + deltaX) 159 | newRect.size.height = max(20, newRect.size.height + deltaY) 160 | case .right: 161 | newRect.size.width = max(20, newRect.size.width + deltaX) 162 | case .bottomRight: 163 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 164 | newRect.size.width = max(20, newRect.size.width + deltaX) 165 | newRect.size.height = max(20, newRect.size.height - deltaY) 166 | case .bottom: 167 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 168 | newRect.size.height = max(20, newRect.size.height - deltaY) 169 | case .bottomLeft: 170 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 171 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 172 | newRect.size.width = max(20, newRect.size.width - deltaX) 173 | newRect.size.height = max(20, newRect.size.height - deltaY) 174 | case .left: 175 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 176 | newRect.size.width = max(20, newRect.size.width - deltaX) 177 | default: 178 | break 179 | } 180 | self.selectionRect = newRect 181 | initialLocation = currentLocation // Update initial location for continuous dragging 182 | } else { 183 | if dragIng { 184 | dragIng = true 185 | // 计算移动偏移量 186 | let deltaX = currentLocation.x - initialLocation.x 187 | let deltaY = currentLocation.y - initialLocation.y 188 | 189 | // 更新矩形位置 190 | let x = self.selectionRect.origin.x 191 | let y = self.selectionRect.origin.y 192 | let w = self.selectionRect.size.width 193 | let h = self.selectionRect.size.height 194 | self.selectionRect.origin.x = min(max(0.0, x + deltaX), self.frame.width - w) 195 | self.selectionRect.origin.y = min(max(0.0, y + deltaY), self.frame.height - h) 196 | initialLocation = currentLocation 197 | } else { 198 | // 创建新矩形 199 | guard let maxFrame = maxFrame else { return } 200 | let origin = NSPoint(x: max(maxFrame.origin.x, min(initialLocation.x, currentLocation.x)), y: max(maxFrame.origin.y, min(initialLocation.y, currentLocation.y))) 201 | var maxH = abs(currentLocation.y - initialLocation.y) 202 | var maxW = abs(currentLocation.x - initialLocation.x) 203 | if currentLocation.y < maxFrame.origin.y { maxH = initialLocation.y } 204 | if currentLocation.x < maxFrame.origin.x { maxW = initialLocation.x } 205 | let size = NSSize(width: maxW, height: maxH) 206 | self.selectionRect = NSIntersectionRect(maxFrame, NSRect(origin: origin, size: size)) 207 | } 208 | self.initialLocation = initialLocation 209 | } 210 | lastMouseLocation = currentLocation 211 | needsDisplay = true 212 | } 213 | 214 | override func mouseDown(with event: NSEvent) { 215 | let location = convert(event.locationInWindow, from: nil) 216 | if self.isOnBorderAt(location) { 217 | self.dragIng = true 218 | } 219 | else { 220 | initialLocation = location 221 | lastMouseLocation = location 222 | activeHandle = handleForPoint(location) 223 | } 224 | 225 | needsDisplay = true 226 | } 227 | 228 | override func mouseUp(with event: NSEvent) { 229 | // initialLocation = nil 230 | activeHandle = .none 231 | dragIng = false 232 | needsDisplay = true 233 | print("lt arrow mouse up, \(String(describing: self.initialLocation)) \(String(describing: self.lastMouseLocation))") 234 | } 235 | 236 | override func hitTest(_ point: NSPoint) -> NSView? { 237 | let hitView = super.hitTest(point) 238 | if hitView == self { 239 | return self.superview 240 | } 241 | return hitView 242 | } 243 | } 244 | 245 | 246 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/MainOverlay/showView/ScreenshotTextView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import ScreenCaptureKit 4 | import AppKit 5 | import Combine 6 | 7 | // 文本内容 8 | class ScreenshotTextView: ScreenshotBaseOverlayView , NSTextViewDelegate{ 9 | var maxFrame: NSRect? 10 | var fillOverLayeralpha: CGFloat = 0.0 // 默认值 11 | var textSize: CGFloat = 12.0 12 | var textView: NSTextView = NSTextView(frame: NSMakeRect(0, 0, 0, 0)) 13 | var textIsEditing = false 14 | var dragIng: Bool = false 15 | var lastMouseLoc: NSPoint? 16 | let controlPointDiameter: CGFloat = 8.0 17 | var textCancellables = Set() 18 | var activeHandle: RetangleResizeHandle = .none 19 | 20 | var hasSelectionRect: Bool { 21 | return (self.textView.frame.size.width > 0 && self.textView.frame.size.height > 0) 22 | } 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | self.lineWidth = 2.0 27 | self.textView.backgroundColor = .clear 28 | self.textView.isVerticallyResizable = true 29 | self.textView.textContainer?.widthTracksTextView = true // 让宽度自动跟踪 textView 的宽度 30 | self.addSubview(self.textView) 31 | } 32 | 33 | func update() { 34 | self.textView.textColor = self.selectedColor 35 | self.textView.font = .systemFont(ofSize: self.textSize) 36 | self.needsLayout = true 37 | } 38 | 39 | required init?(coder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | 43 | override func viewDidMoveToWindow() { 44 | super.viewDidMoveToWindow() 45 | } 46 | 47 | func textShouldEndEditing(_ textObject: NSText) -> Bool { 48 | return true 49 | } 50 | 51 | func textDidBeginEditing(_ notification: Notification) { 52 | self.textIsEditing = true 53 | } 54 | 55 | func textDidEndEditing(_ notification: Notification) { 56 | print("textDidEndEditing") 57 | self.textIsEditing = false 58 | } 59 | 60 | func textDidChange(_ notification: Notification) { 61 | self.needsDisplay = true 62 | } 63 | 64 | func isEmptyText() -> Bool { 65 | let curString: String = self.textView.string.trimmingCharacters(in: .whitespacesAndNewlines) 66 | return curString.count == 0 67 | } 68 | 69 | override func mouseDown(with event: NSEvent) { 70 | let location = convert(event.locationInWindow, from: nil) 71 | if (self.textView.frame.size.width <= 1.0) { 72 | self.textView.frame = NSMakeRect(location.x, location.y, self.textSize * 6, self.textSize + 6) 73 | self.textView.delegate = self; 74 | self.window?.makeFirstResponder(self.textView) 75 | self.textView.font = .systemFont(ofSize: self.textSize) 76 | self.textView.textColor = self.selectedColor 77 | self.textView.layer?.cornerRadius = 4.0 78 | self.textView.wantsLayer = true 79 | self.textView.layer?.borderWidth = 2.0 80 | self.textView.layer?.borderColor = NSColor.gray.cgColor 81 | self.textView.layer?.masksToBounds = true 82 | } 83 | else { 84 | self.window?.makeFirstResponder(nil) 85 | self.activeHandle = handleForPoint(location) 86 | if self.isOnBorderAt(location) { 87 | self.dragIng = true 88 | self.lastMouseLoc = convert(event.locationInWindow, from: nil) 89 | } 90 | 91 | if self.textView.string.count == 0 { 92 | self.textView.frame = NSRect.zero 93 | } 94 | } 95 | } 96 | 97 | override func mouseDragged(with event: NSEvent) { 98 | let loc = convert(event.locationInWindow, from: nil) 99 | 100 | if activeHandle != .none { 101 | var newRect = self.textView.frame 102 | let lastLocation = self.lastMouseLoc 103 | 104 | let deltaX = loc.x - lastLocation!.x 105 | let deltaY = loc.y - lastLocation!.y 106 | print("tt -- dragged Move : \(deltaX) , deltaY: \(deltaY)") 107 | 108 | switch activeHandle { 109 | case .topLeft: 110 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 111 | newRect.size.width = max(20, newRect.size.width - deltaX) 112 | newRect.size.height = max(20, newRect.size.height + deltaY) 113 | case .top: 114 | newRect.size.height = max(20, newRect.size.height + deltaY) 115 | case .topRight: 116 | newRect.size.width = max(20, newRect.size.width + deltaX) 117 | newRect.size.height = max(20, newRect.size.height + deltaY) 118 | case .right: 119 | newRect.size.width = max(20, newRect.size.width + deltaX) 120 | case .bottomRight: 121 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 122 | newRect.size.width = max(20, newRect.size.width + deltaX) 123 | newRect.size.height = max(20, newRect.size.height - deltaY) 124 | case .bottom: 125 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 126 | newRect.size.height = max(20, newRect.size.height - deltaY) 127 | case .bottomLeft: 128 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 129 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 130 | newRect.size.width = max(20, newRect.size.width - deltaX) 131 | newRect.size.height = max(20, newRect.size.height - deltaY) 132 | case .left: 133 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 134 | newRect.size.width = max(20, newRect.size.width - deltaX) 135 | default: 136 | break 137 | } 138 | self.textView.frame = newRect 139 | print("lt -- new rect : \(newRect)") 140 | } 141 | else if (self.dragIng && self.lastMouseLoc != nil) { 142 | let detaX = loc.x - lastMouseLoc!.x 143 | let detaY = loc.y - lastMouseLoc!.y 144 | let origin = NSMakePoint(self.textView.frame.origin.x + detaX, self.textView.frame.origin.y + detaY) 145 | self.textView.setFrameOrigin(origin) 146 | } 147 | self.lastMouseLoc = loc 148 | self.needsDisplay = true 149 | } 150 | 151 | override func mouseUp(with event: NSEvent) { 152 | print("lt -- subview mosueup") 153 | if (self.dragIng) { 154 | self.dragIng = false 155 | } 156 | } 157 | 158 | override func isOnBorderAt(_ point: NSPoint) -> Bool { 159 | if self.textView.bounds.size.width <= 1 { 160 | return false 161 | } 162 | 163 | return NSPoint.isPointAtFrame(point: point ,rect: self.textView.frame) 164 | } 165 | 166 | override func draw(_ dirtyRect: NSRect) { 167 | super.draw(dirtyRect) 168 | maxFrame = dirtyRect 169 | 170 | NSColor.clear.withAlphaComponent(self.fillOverLayeralpha).setFill() 171 | dirtyRect.fill() 172 | 173 | if !self.hasSelectionRect { 174 | return 175 | } 176 | 177 | let rect = self.textView.frame 178 | // 绘制边框 179 | let dashedBorder = NSBezierPath(rect: rect) 180 | dashedBorder.lineWidth = lineWidth 181 | NSColor.gray.setStroke() 182 | dashedBorder.stroke() 183 | NSColor.init(white: 1, alpha: 0.01).setFill() 184 | // selectedColor.setFill() 185 | __NSRectFill(rect) 186 | // 绘制边框中的点 187 | if (!self.editFinished) { 188 | for handle in RetangleResizeHandle.allCases { 189 | if let point = controlPointForHandle(handle, inRect: rect) { 190 | let controlPointRect = NSRect(origin: point, size: CGSize(width: controlPointDiameter, height: controlPointDiameter)) 191 | let controlPointPath = NSBezierPath(ovalIn: controlPointRect) 192 | NSColor.white.setFill() 193 | controlPointPath.fill() 194 | } 195 | } 196 | } 197 | } 198 | 199 | func controlPointForHandle(_ handle: RetangleResizeHandle, inRect rect: NSRect) -> NSPoint? { 200 | switch handle { 201 | case .topLeft: 202 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.maxY - controlPointDiameter / 2 + 1) 203 | case .top: 204 | return NSPoint(x: rect.midX - controlPointDiameter / 2, y: rect.maxY - controlPointDiameter / 2 + 1) 205 | case .topRight: 206 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.maxY - controlPointDiameter / 2 + 1) 207 | case .right: 208 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.midY - controlPointDiameter / 2) 209 | case .bottomRight: 210 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.minY - controlPointDiameter / 2 - 1) 211 | case .bottom: 212 | return NSPoint(x: rect.midX - controlPointDiameter / 2, y: rect.minY - controlPointDiameter / 2 - 1) 213 | case .bottomLeft: 214 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.minY - controlPointDiameter / 2 - 1) 215 | case .left: 216 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.midY - controlPointDiameter / 2) 217 | case .none: 218 | return nil 219 | } 220 | } 221 | 222 | override func handleForPoint(_ point: NSPoint) -> RetangleResizeHandle { 223 | if self.textView.frame.size.width < kSelectionMinWidth { 224 | return .none 225 | } 226 | let rect = self.textView.frame 227 | for handle in RetangleResizeHandle.allCases { 228 | if let controlPoint = controlPointForHandle(handle, inRect: rect), NSRect(origin: controlPoint, size: CGSize(width: controlPointDiameter, height: controlPointDiameter)).contains(point) { 229 | return handle 230 | } 231 | } 232 | return .none 233 | } 234 | 235 | override func handleborderForPoint(_ point: NSPoint) -> RetangleResizeHandle { 236 | if self.textView.frame.size.width < kSelectionMinWidth { 237 | return .none 238 | } 239 | let deta = controlPointDiameter / 2 240 | let rect = self.textView.frame 241 | // 上 242 | if (point.x > rect.minX - deta && point.x < rect.maxX + deta && point.y > rect.maxY - deta && point.y < rect.maxY + deta) { 243 | return .top 244 | } 245 | 246 | // 下 247 | if (point.x > rect.minX - deta && point.x < rect.maxX + deta && point.y > rect.minY - deta && point.y < rect.minY + deta) { 248 | return .bottom 249 | } 250 | 251 | // 左 252 | if (point.x > rect.minX - deta && point.x < rect.minX + deta && point.y > rect.minY - deta && point.y < rect.maxY + deta) { 253 | return .left 254 | } 255 | // 右 256 | if (point.x > rect.maxX - deta && point.x < rect.maxX + deta && point.y > rect.minY - deta && point.y < rect.maxY + deta) { 257 | return .right 258 | } 259 | 260 | return .none 261 | } 262 | 263 | override func hitTest(_ point: NSPoint) -> NSView? { 264 | let hitView = super.hitTest(point) 265 | if hitView == self { 266 | return self.superview 267 | } 268 | return hitView 269 | } 270 | } 271 | 272 | 273 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2024] [helinyu] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/MainOverlay/showView/ScreenshotRectangleView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import ScreenCaptureKit 4 | import AppKit 5 | 6 | let kSelectionMinWidth = 2.0 7 | 8 | // 矩形的属性 9 | class ScreenshotRectangleView: ScreenshotBaseOverlayView { 10 | 11 | var selectionRect: NSRect = NSRect.zero // 默认是zero 12 | // 这个应该是可以只留下一个的 13 | var initialLocation: NSPoint? 14 | var lastMouseLocation: NSPoint? 15 | 16 | var dragIng: Bool = false 17 | var activeHandle: RetangleResizeHandle = .none 18 | var maxFrame: NSRect? 19 | let controlPointDiameter: CGFloat = 8.0 20 | let controlPointColor: NSColor = NSColor.white 21 | var fillOverLayeralpha: CGFloat = 0.0 // 默认值 22 | // var selectedColor: NSColor = NSColor.white 23 | // var lineWidth: CGFloat = 4.0 24 | 25 | var hasSelectionRect: Bool { 26 | return (self.selectionRect.size.width > 0 && self.selectionRect.size.height > 0) 27 | } 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | override func viewDidMoveToWindow() { 38 | super.viewDidMoveToWindow() 39 | 40 | selectionRect = NSRect.zero 41 | } 42 | 43 | override func draw(_ dirtyRect: NSRect) { 44 | super.draw(dirtyRect) 45 | maxFrame = dirtyRect 46 | 47 | // 暂时先这样吧 48 | NSColor.clear.withAlphaComponent(self.fillOverLayeralpha).setFill() 49 | 50 | dirtyRect.fill() 51 | 52 | if !self.hasSelectionRect { 53 | return 54 | } 55 | 56 | let rect = selectionRect 57 | // 绘制边框 58 | let dashedBorder = NSBezierPath(rect: rect) 59 | dashedBorder.lineWidth = lineWidth 60 | selectedColor.setStroke() 61 | dashedBorder.stroke() 62 | NSColor.init(white: 1, alpha: 0.01).setFill() 63 | // selectedColor.setFill() 64 | __NSRectFill(rect) 65 | // 绘制边框中的点 66 | if (!self.editFinished) { 67 | for handle in RetangleResizeHandle.allCases { 68 | if let point = controlPointForHandle(handle, inRect: rect) { 69 | let controlPointRect = NSRect(origin: point, size: CGSize(width: controlPointDiameter, height: controlPointDiameter)) 70 | let controlPointPath = NSBezierPath(ovalIn: controlPointRect) 71 | controlPointColor.setFill() 72 | controlPointPath.fill() 73 | } 74 | } 75 | } 76 | } 77 | 78 | override func handleForPoint(_ point: NSPoint) -> RetangleResizeHandle { 79 | if self.selectionRect.size.width < kSelectionMinWidth { 80 | return .none 81 | } 82 | let rect = self.selectionRect 83 | for handle in RetangleResizeHandle.allCases { 84 | if let controlPoint = controlPointForHandle(handle, inRect: rect), NSRect(origin: controlPoint, size: CGSize(width: controlPointDiameter, height: controlPointDiameter)).contains(point) { 85 | return handle 86 | } 87 | } 88 | return .none 89 | } 90 | 91 | override func handleborderForPoint(_ point: NSPoint) -> RetangleResizeHandle { 92 | if self.selectionRect.size.width < kSelectionMinWidth { 93 | return .none 94 | } 95 | let deta = controlPointDiameter / 2 96 | let rect = self.selectionRect 97 | // 上 98 | if (point.x > rect.minX - deta && point.x < rect.maxX + deta && point.y > rect.maxY - deta && point.y < rect.maxY + deta) { 99 | return .top 100 | } 101 | 102 | // 下 103 | if (point.x > rect.minX - deta && point.x < rect.maxX + deta && point.y > rect.minY - deta && point.y < rect.minY + deta) { 104 | return .bottom 105 | } 106 | 107 | // 左 108 | if (point.x > rect.minX - deta && point.x < rect.minX + deta && point.y > rect.minY - deta && point.y < rect.maxY + deta) { 109 | return .left 110 | } 111 | // 右 112 | if (point.x > rect.maxX - deta && point.x < rect.maxX + deta && point.y > rect.minY - deta && point.y < rect.maxY + deta) { 113 | return .right 114 | } 115 | 116 | return .none 117 | } 118 | 119 | func controlPointForHandle(_ handle: RetangleResizeHandle, inRect rect: NSRect) -> NSPoint? { 120 | switch handle { 121 | case .topLeft: 122 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.maxY - controlPointDiameter / 2 + 1) 123 | case .top: 124 | return NSPoint(x: rect.midX - controlPointDiameter / 2, y: rect.maxY - controlPointDiameter / 2 + 1) 125 | case .topRight: 126 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.maxY - controlPointDiameter / 2 + 1) 127 | case .right: 128 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.midY - controlPointDiameter / 2) 129 | case .bottomRight: 130 | return NSPoint(x: rect.maxX - controlPointDiameter / 2 + 1, y: rect.minY - controlPointDiameter / 2 - 1) 131 | case .bottom: 132 | return NSPoint(x: rect.midX - controlPointDiameter / 2, y: rect.minY - controlPointDiameter / 2 - 1) 133 | case .bottomLeft: 134 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.minY - controlPointDiameter / 2 - 1) 135 | case .left: 136 | return NSPoint(x: rect.minX - controlPointDiameter / 2 - 1, y: rect.midY - controlPointDiameter / 2) 137 | case .none: 138 | return nil 139 | } 140 | } 141 | 142 | override func mouseDragged(with event: NSEvent) { 143 | // print("lt -- inner view mouseDragged 0 : \(self) \(self.dragIng)") 144 | if (self.editFinished) { 145 | return 146 | } 147 | 148 | // print("lt -- inner view mouseDragged 1 : \(self)") 149 | guard var initialLocation = initialLocation else { return } 150 | let currentLocation = convert(event.locationInWindow, from: nil) 151 | 152 | if activeHandle != .none { 153 | var newRect = selectionRect 154 | let lastLocation = lastMouseLocation ?? currentLocation 155 | 156 | let deltaX = currentLocation.x - lastLocation.x 157 | let deltaY = currentLocation.y - lastLocation.y 158 | 159 | switch activeHandle { 160 | case .topLeft: 161 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 162 | newRect.size.width = max(20, newRect.size.width - deltaX) 163 | newRect.size.height = max(20, newRect.size.height + deltaY) 164 | case .top: 165 | newRect.size.height = max(20, newRect.size.height + deltaY) 166 | case .topRight: 167 | newRect.size.width = max(20, newRect.size.width + deltaX) 168 | newRect.size.height = max(20, newRect.size.height + deltaY) 169 | case .right: 170 | newRect.size.width = max(20, newRect.size.width + deltaX) 171 | case .bottomRight: 172 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 173 | newRect.size.width = max(20, newRect.size.width + deltaX) 174 | newRect.size.height = max(20, newRect.size.height - deltaY) 175 | case .bottom: 176 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 177 | newRect.size.height = max(20, newRect.size.height - deltaY) 178 | case .bottomLeft: 179 | newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) 180 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 181 | newRect.size.width = max(20, newRect.size.width - deltaX) 182 | newRect.size.height = max(20, newRect.size.height - deltaY) 183 | case .left: 184 | newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) 185 | newRect.size.width = max(20, newRect.size.width - deltaX) 186 | default: 187 | break 188 | } 189 | self.selectionRect = newRect 190 | initialLocation = currentLocation // Update initial location for continuous dragging 191 | } else { 192 | if self.dragIng { 193 | // print("lt -- draging : \(self.dragIng)") 194 | // 计算移动偏移量 195 | let deltaX = currentLocation.x - initialLocation.x 196 | let deltaY = currentLocation.y - initialLocation.y 197 | 198 | // 更新矩形位置 199 | let x = self.selectionRect.origin.x 200 | let y = self.selectionRect.origin.y 201 | let w = self.selectionRect.size.width 202 | let h = self.selectionRect.size.height 203 | // print("lt -- select rect: \(self.selectionRect)") 204 | self.selectionRect.origin.x = min(max(0.0, x + deltaX), self.frame.width - w) 205 | self.selectionRect.origin.y = min(max(0.0, y + deltaY), self.frame.height - h) 206 | // print("lt -- select rect 1: \(self.selectionRect)") 207 | initialLocation = currentLocation 208 | } else { 209 | // 创建新矩形 210 | guard let maxFrame = maxFrame else { return } 211 | let origin = NSPoint(x: max(maxFrame.origin.x, min(initialLocation.x, currentLocation.x)), y: max(maxFrame.origin.y, min(initialLocation.y, currentLocation.y))) 212 | var maxH = abs(currentLocation.y - initialLocation.y) 213 | var maxW = abs(currentLocation.x - initialLocation.x) 214 | if currentLocation.y < maxFrame.origin.y { maxH = initialLocation.y } 215 | if currentLocation.x < maxFrame.origin.x { maxW = initialLocation.x } 216 | let size = NSSize(width: maxW, height: maxH) 217 | self.selectionRect = NSIntersectionRect(maxFrame, NSRect(origin: origin, size: size)) 218 | } 219 | self.initialLocation = initialLocation 220 | } 221 | lastMouseLocation = currentLocation 222 | needsDisplay = true 223 | } 224 | 225 | override func mouseDown(with event: NSEvent) { 226 | // print("lt -- son inner view mouseDown 0 : \(self)") 227 | if (self.editFinished) { 228 | return 229 | } 230 | let location = convert(event.locationInWindow, from: nil) 231 | // print("lt -- inner view mouseDown 1: \(event.locationInWindow) \(location)") 232 | 233 | initialLocation = location 234 | lastMouseLocation = location 235 | activeHandle = handleForPoint(location) 236 | let borderHandle = self.handleborderForPoint(location) 237 | // print("lt -- mousedown borderHandle : \(borderHandle)") 238 | if (borderHandle != .none) { 239 | self.dragIng = true 240 | } 241 | self.needsDisplay = true 242 | } 243 | 244 | override func mouseUp(with event: NSEvent) { 245 | // print("lt -- sone mouseup") 246 | if (self.editFinished) { 247 | return 248 | } 249 | initialLocation = nil 250 | activeHandle = .none 251 | dragIng = false 252 | self.needsDisplay = true 253 | } 254 | 255 | 256 | // 这样是为了让mouseDown在superView中监听到来调用子View的方法 257 | override func hitTest(_ point: NSPoint) -> NSView? { 258 | let hitView = super.hitTest(point) 259 | if hitView == self && hitView is ScreenshotRectangleView && hitView as? ScreenshotRectangleView !== hitView as? ScreenshotOverlayView { 260 | // print("对象是 ParentClass 类型而不是 ChildClass(或其他子类)") 261 | // print("lt -- 当前子类的页面传递处理") 262 | return self.superview 263 | } 264 | return hitView 265 | } 266 | } 267 | 268 | -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/SwiftUIComponents/SwiftUIIntegration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIIntegration.swift 3 | // ScreenCut 4 | // 5 | // Created by helinyu on 2024/12/19. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | import Combine 11 | 12 | // MARK: - SwiftUI Integration Manager 13 | class SwiftUIIntegrationManager: ObservableObject { 14 | static let shared = SwiftUIIntegrationManager() 15 | 16 | @Published var isScreenshotMode = false 17 | @Published var currentScreenshotWindow: SwiftUIScreenshotWindowController? 18 | 19 | private init() {} 20 | 21 | func startScreenshotMode() { 22 | isScreenshotMode = true 23 | currentScreenshotWindow = SwiftUIScreenshotWindowController() 24 | currentScreenshotWindow?.showWindow(nil) 25 | } 26 | 27 | func endScreenshotMode() { 28 | isScreenshotMode = false 29 | currentScreenshotWindow?.close() 30 | currentScreenshotWindow = nil 31 | } 32 | } 33 | 34 | // MARK: - Enhanced Screenshot Overlay View 35 | struct EnhancedScreenshotOverlayView: View { 36 | @StateObject private var bottomEditItem = EditCutBottomShareModel.shared 37 | @ObservedObject private var actionItem = EditActionShareModel.shared 38 | @StateObject private var integrationManager = SwiftUIIntegrationManager.shared 39 | 40 | @State private var selectionRect = CGRect.zero 41 | @State private var hasSelectionRect = false 42 | @State private var isDragging = false 43 | @State private var dragStartPoint = CGPoint.zero 44 | @State private var operViews: [AnyView] = [] 45 | @State private var currentOperViewIndex: Int? 46 | @State private var isEditFinished = false 47 | @State private var isFindForDown = false 48 | 49 | @State private var cancellables = Set() 50 | @State private var bottomPanelVisible = false 51 | 52 | var body: some View { 53 | ZStack { 54 | // Background overlay 55 | Rectangle() 56 | .fill(Color.gray.opacity(0.3)) 57 | .ignoresSafeArea() 58 | .onTapGesture { location in 59 | if !hasSelectionRect { 60 | startSelection(at: location) 61 | } 62 | } 63 | 64 | // Selection rectangle 65 | if hasSelectionRect { 66 | SelectionRectangleView( 67 | rect: selectionRect, 68 | isDragging: isDragging, 69 | onDragChanged: handleSelectionDragChanged, 70 | onDragEnded: handleSelectionDragEnded 71 | ) 72 | } 73 | 74 | // Drawing views 75 | ForEach(operViews.indices, id: \.self) { index in 76 | operViews[index] 77 | .position( 78 | x: operViews[index].frame?.midX ?? 0, 79 | y: operViews[index].frame?.midY ?? 0 80 | ) 81 | } 82 | 83 | // Bottom edit panel 84 | if bottomPanelVisible { 85 | VStack { 86 | Spacer() 87 | EditCutBottomView() 88 | .background(Color.black.opacity(0.7)) 89 | .cornerRadius(8) 90 | .padding() 91 | } 92 | } 93 | } 94 | .onAppear { 95 | setupNotifications() 96 | } 97 | .onDisappear { 98 | cleanup() 99 | } 100 | .focusable() 101 | .onKeyPress { keyPress in 102 | handleKeyPress(keyPress) 103 | } 104 | } 105 | 106 | private func setupNotifications() { 107 | NotificationCenter.default.publisher(for: .kCutTypeChange) 108 | .merge(with: NotificationCenter.default.publisher(for: .kSelectColorTypeChange)) 109 | .merge(with: NotificationCenter.default.publisher(for: .kDrawSizeTypeChange)) 110 | .merge(with: NotificationCenter.default.publisher(for: .kTextSizeTypeChange)) 111 | .sink { notification in 112 | handleNotification(notification) 113 | } 114 | .store(in: &cancellables) 115 | 116 | NotificationCenter.default.publisher(for: .kDownloadClick) 117 | .sink { _ in 118 | handleDownloadClick() 119 | } 120 | .store(in: &cancellables) 121 | } 122 | 123 | private func handleNotification(_ notification: Notification) { 124 | switch notification.name { 125 | case .kCutTypeChange: 126 | isEditFinished = true 127 | case .kSelectColorTypeChange, .kDrawSizeTypeChange, .kTextSizeTypeChange: 128 | updateCurrentOperView() 129 | default: 130 | break 131 | } 132 | } 133 | 134 | private func handleDownloadClick() { 135 | if !isEditFinished { 136 | isEditFinished = true 137 | } 138 | bottomPanelVisible = false 139 | } 140 | 141 | private func startSelection(at location: CGPoint) { 142 | hasSelectionRect = true 143 | dragStartPoint = location 144 | selectionRect = CGRect(origin: location, size: .zero) 145 | } 146 | 147 | private func handleSelectionDragChanged(_ value: DragGesture.Value) { 148 | if !hasSelectionRect { return } 149 | 150 | let currentPoint = value.location 151 | let minX = min(dragStartPoint.x, currentPoint.x) 152 | let minY = min(dragStartPoint.y, currentPoint.y) 153 | let maxX = max(dragStartPoint.x, currentPoint.x) 154 | let maxY = max(dragStartPoint.y, currentPoint.y) 155 | 156 | selectionRect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) 157 | isDragging = true 158 | } 159 | 160 | private func handleSelectionDragEnded(_ value: DragGesture.Value) { 161 | isDragging = false 162 | if hasSelectionRect && selectionRect.width > 10 && selectionRect.height > 10 { 163 | ScreenCut.screenArea = selectionRect 164 | bottomPanelVisible = true 165 | } 166 | } 167 | 168 | private func updateCurrentOperView() { 169 | guard let index = currentOperViewIndex, 170 | index < operViews.count else { return } 171 | 172 | // Update the current oper view properties 173 | // This would need to be implemented based on the specific drawing view type 174 | } 175 | 176 | private func handleKeyPress(_ keyPress: KeyPress) -> KeyPress.Result { 177 | switch keyPress.key { 178 | case .return: 179 | actionItem.actionType = .download 180 | return .handled 181 | case .delete: 182 | deleteCurrentOperView() 183 | return .handled 184 | case KeyEquivalent("z") where keyPress.modifiers.contains(.command): 185 | undoLastOperation() 186 | return .handled 187 | case .escape: 188 | integrationManager.endScreenshotMode() 189 | return .handled 190 | default: 191 | return .ignored 192 | } 193 | } 194 | 195 | private func deleteCurrentOperView() { 196 | guard let index = currentOperViewIndex, 197 | index < operViews.count else { 198 | showToast(message: "目前没有选中要删除的页面") 199 | return 200 | } 201 | 202 | operViews.remove(at: index) 203 | currentOperViewIndex = nil 204 | } 205 | 206 | private func undoLastOperation() { 207 | guard !operViews.isEmpty else { 208 | showToast(message: "没有操作可回退了") 209 | return 210 | } 211 | 212 | operViews.removeLast() 213 | currentOperViewIndex = nil 214 | } 215 | 216 | private func showToast(message: String) { 217 | ToastController.shared.showToast(message: message) 218 | } 219 | 220 | private func cleanup() { 221 | cancellables.removeAll() 222 | } 223 | } 224 | 225 | // MARK: - Selection Rectangle View 226 | struct SelectionRectangleView: View { 227 | let rect: CGRect 228 | let isDragging: Bool 229 | let onDragChanged: (DragGesture.Value) -> Void 230 | let onDragEnded: (DragGesture.Value) -> Void 231 | 232 | var body: some View { 233 | Rectangle() 234 | .stroke(Color.white, lineWidth: 2) 235 | .frame(width: rect.width, height: rect.height) 236 | .position(x: rect.midX, y: rect.midY) 237 | .gesture( 238 | DragGesture(minimumDistance: 0) 239 | .onChanged(onDragChanged) 240 | .onEnded(onDragEnded) 241 | ) 242 | } 243 | } 244 | 245 | // MARK: - Enhanced Drawing Views 246 | struct EnhancedRectangleDrawingView: View { 247 | @State var frame: CGRect 248 | @State var isEditFinished = false 249 | @State var selectedColor: Color = .white 250 | @State var lineWidth: CGFloat = 2.0 251 | 252 | var body: some View { 253 | Rectangle() 254 | .stroke(selectedColor, lineWidth: lineWidth) 255 | .frame(width: frame.width, height: frame.height) 256 | .gesture( 257 | DragGesture(minimumDistance: 0) 258 | .onChanged { value in 259 | if !isEditFinished { 260 | frame.origin = value.location 261 | } 262 | } 263 | ) 264 | } 265 | } 266 | 267 | struct EnhancedCircleDrawingView: View { 268 | @State var frame: CGRect 269 | @State var isEditFinished = false 270 | @State var selectedColor: Color = .white 271 | @State var lineWidth: CGFloat = 2.0 272 | 273 | var body: some View { 274 | Circle() 275 | .stroke(selectedColor, lineWidth: lineWidth) 276 | .frame(width: frame.width, height: frame.height) 277 | .gesture( 278 | DragGesture(minimumDistance: 0) 279 | .onChanged { value in 280 | if !isEditFinished { 281 | frame.origin = value.location 282 | } 283 | } 284 | ) 285 | } 286 | } 287 | 288 | struct EnhancedArrowDrawingView: View { 289 | @State var frame: CGRect 290 | @State var isEditFinished = false 291 | @State var selectedColor: Color = .white 292 | @State var lineWidth: CGFloat = 2.0 293 | 294 | var body: some View { 295 | Path { path in 296 | path.move(to: CGPoint(x: frame.minX, y: frame.midY)) 297 | path.addLine(to: CGPoint(x: frame.maxX, y: frame.midY)) 298 | path.addLine(to: CGPoint(x: frame.maxX - 10, y: frame.midY - 5)) 299 | path.move(to: CGPoint(x: frame.maxX, y: frame.midY)) 300 | path.addLine(to: CGPoint(x: frame.maxX - 10, y: frame.midY + 5)) 301 | } 302 | .stroke(selectedColor, lineWidth: lineWidth) 303 | .gesture( 304 | DragGesture(minimumDistance: 0) 305 | .onChanged { value in 306 | if !isEditFinished { 307 | frame.origin = value.location 308 | } 309 | } 310 | ) 311 | } 312 | } 313 | 314 | struct EnhancedTextDrawingView: View { 315 | @State var frame: CGRect 316 | @State var isEditFinished = false 317 | @State var selectedColor: Color = .white 318 | @State var fontSize: CGFloat = 12 319 | @State var text: String = "" 320 | @FocusState private var isTextFieldFocused: Bool 321 | 322 | var body: some View { 323 | TextField("输入文字", text: $text) 324 | .font(.system(size: fontSize)) 325 | .foregroundColor(selectedColor) 326 | .frame(width: frame.width, height: frame.height) 327 | .background(Color.clear) 328 | .focused($isTextFieldFocused) 329 | .onTapGesture { 330 | isTextFieldFocused = true 331 | } 332 | } 333 | } 334 | 335 | // MARK: - Extensions for AnyView 336 | extension AnyView { 337 | var frame: CGRect? { 338 | // This would need to be implemented to track frame information 339 | // For now, return a default frame 340 | return CGRect(x: 0, y: 0, width: 100, height: 50) 341 | } 342 | } -------------------------------------------------------------------------------- /ScreenCut/ScreenCut/Action/ScreenCut.swift: -------------------------------------------------------------------------------- 1 | import ScreenCaptureKit 2 | import Cocoa 3 | import FileKit 4 | import SwiftUI 5 | import Vision 6 | import Moya 7 | import Combine 8 | 9 | 10 | var cancellables = Set() 11 | 12 | 13 | func findCurrentScreen(id: CGDirectDisplayID, displays:[SCDisplay]) -> SCDisplay? { 14 | for display in displays { 15 | if (id == display.displayID) { 16 | return display 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // 这个是查找Screen的内容 23 | func findCurrentScreen(id: CGDirectDisplayID, screens:[NSScreen]) -> NSScreen? { 24 | for screen in screens { 25 | if (id == screen.displayID) { 26 | return screen 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | 33 | class ScreenCut { 34 | 35 | static var availableContent: SCShareableContent? 36 | static var screenArea: NSRect? 37 | 38 | func closeAllWindow(except: String = "") { 39 | for w in NSApp.windows.filter({ 40 | $0.title != "Item-0" && $0.title != "" 41 | && !$0.title.lowercased().contains(".qma") 42 | && !$0.title.contains(except) }) { w.close() } 43 | } 44 | 45 | static func updateScreenContent() async { 46 | SCShareableContent.getExcludingDesktopWindows(false, onScreenWindowsOnly: false) { content, error in 47 | if let error = error { 48 | switch error { 49 | case SCStreamError.userDeclined: Auth.requestPermissions() 50 | default: print("Error: failed to fetch available content: ", error.localizedDescription) 51 | } 52 | return 53 | } 54 | 55 | ScreenCut.availableContent = content 56 | } 57 | } 58 | 59 | // 保存整张图片 60 | @MainActor static func saveScreenFullImage() { 61 | let content = ScreenCut.availableContent 62 | guard let displays = content?.displays else { 63 | return 64 | } 65 | let display: SCDisplay = findCurrentScreen( 66 | id: AppDelegate.shared.screentId!, 67 | displays: displays 68 | )! 69 | let contentFilter = SCContentFilter(display: display, excludingWindows: []) 70 | let configuration = SCStreamConfiguration() 71 | configuration.width = display.width 72 | configuration.height = display.height 73 | SCScreenshotManager.captureImage(contentFilter: contentFilter, configuration: configuration) { image, error in 74 | print("lt -- image : eror : %@", error.debugDescription) 75 | guard let img = image else { 76 | print(" : %@", error.debugDescription) 77 | return 78 | } 79 | self.onlyPasteboardOfSameTime(img) 80 | } 81 | } 82 | 83 | // 保存图片 84 | @MainActor static func saveImageToFile(_ image: CGImage) { 85 | let imgName = Date.getNameByDate() 86 | let curPath = "file://" + VarExtension.createTargetDirIfNotExit() + "/" + imgName + ".png" 87 | let destinationURL: CFURL = URL(string: curPath)! as CFURL 88 | let destination = CGImageDestinationCreateWithURL(destinationURL, kUTTypePNG, 1, nil) 89 | guard let destination = destination else { 90 | // print("保存路径没有创建成功") 91 | return 92 | } 93 | CGImageDestinationAddImage(destination, image, nil) 94 | 95 | if CGImageDestinationFinalize(destination) { 96 | print("保存成功路径: \(destinationURL)") 97 | } else { 98 | print("保存失败") 99 | } 100 | } 101 | 102 | // 显示识别的内容 103 | static func showOCR() { 104 | self.cutSelectionAreaImage() 105 | .flatMap { cgImage in 106 | self.getTexWithOCR(cgImage) 107 | } 108 | .receive(on: DispatchQueue.main) // 切换到主线程 109 | .sink( 110 | receiveCompletion: { completion in 111 | print("lt -- \(completion)") 112 | }, 113 | receiveValue: { text in 114 | print("lt -- text: \(text)") 115 | OCRViewWindowController(transText: text).showWindow(nil) 116 | } 117 | ) 118 | .store(in: &cancellables) 119 | } 120 | 121 | 122 | static func copyImageToPasteboard(_ img: CGImage) { 123 | let image: NSImage = cgImageToNSImage(img) 124 | // 创建粘贴板 125 | let pasteboard = NSPasteboard.general 126 | 127 | // 清空当前粘贴板 128 | pasteboard.clearContents() 129 | 130 | // 将图片数据转换为 PNG 数据 131 | if let tiffData = image.tiffRepresentation, 132 | let bitmapRep = NSBitmapImageRep(data: tiffData), 133 | let pngData = bitmapRep.representation(using: .png, properties: [:]) { 134 | // 将 PNG 数据写入粘贴板 135 | pasteboard.setData(pngData, forType: .png) 136 | } 137 | } 138 | 139 | static func cgImageToNSImage(_ cgImage: CGImage) -> NSImage { 140 | // 创建 NSImage 141 | let nsImage = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) 142 | return nsImage 143 | } 144 | 145 | // 识别并且翻译: 业务代码了,可以修改 146 | static func ocrThenTransRequest() { 147 | self.cutSelectionAreaImage() 148 | .flatMap { cgImage in 149 | self.getTexWithOCR(cgImage) ?? Fail(error: NSError(domain: "Error", code: -1, userInfo: nil)).eraseToAnyPublisher() 150 | } 151 | .flatMap { text in 152 | self.transforRequest(text) ?? Fail(error: NSError(domain: "Error", code: -1, userInfo: nil)).eraseToAnyPublisher() 153 | } 154 | .receive(on: DispatchQueue.main) // 切换到主线程 155 | .sink( 156 | receiveCompletion: { completion in 157 | switch completion { 158 | case .finished: 159 | print("操作完成") 160 | case .failure(let error): 161 | print("发生错误: \(error)") 162 | } 163 | }, 164 | receiveValue: { text in 165 | print("转换后的文本: \(text)") 166 | DispatchQueue.main.async { 167 | TranslatorViewWindowController(transText: text).showWindow(nil) 168 | } 169 | } 170 | ) 171 | .store(in: &cancellables) 172 | } 173 | 174 | // combine method base methods 175 | 176 | // 通过图片识别出来文本 177 | static func getTexWithOCR(_ image: CGImage?) -> AnyPublisher { 178 | return Future { promise in 179 | guard let cgImage = image else { 180 | promise(.failure(NSError(domain: "image error", code: -1, userInfo: [NSLocalizedDescriptionKey: "image can not be nil"]))) 181 | return 182 | } 183 | 184 | let request = VNRecognizeTextRequest { request, error in 185 | guard let results = request.results as? [VNRecognizedTextObservation] else { return } 186 | var resultText = "" 187 | for observation in results { 188 | if let topCandidate = observation.topCandidates(1).first { 189 | print("Recognized text: \(topCandidate.string)") 190 | resultText += topCandidate.string 191 | resultText += "\n" 192 | } 193 | } 194 | promise(.success(resultText)) 195 | } 196 | 197 | request.recognitionLevel = .accurate 198 | request.recognitionLanguages = ["zh-Hans"] // 支持简体中文 199 | 200 | let handler = VNImageRequestHandler(cgImage: cgImage , options: [:]) 201 | try? handler.perform([request]) 202 | }.eraseToAnyPublisher() 203 | } 204 | 205 | static func transforRequest(_ text: String) -> AnyPublisher { 206 | return Future { promise in 207 | let urlString = "http://127.0.0.1:5000/translate" // 应该是本地没有解析localhost,要使用127.0.0.1 208 | guard let url = URL(string: urlString) else { return } 209 | 210 | var request = URLRequest(url: url) 211 | request.httpMethod = "POST" 212 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 213 | 214 | let parameters: [String: Any] = ["text": text] // 替换为你的参数 215 | do { 216 | request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) 217 | } catch { 218 | promise(.failure(NSError(domain: "encode error", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error encoding parameters"]))) 219 | return 220 | } 221 | 222 | let task = URLSession.shared.dataTask(with: request) { data, response, error in 223 | do { 224 | if let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any] { 225 | print("Response Dictionary: \(json)") 226 | let translatedText:String = json["translated_text"] as! String 227 | promise(.success(translatedText)) 228 | 229 | } else { 230 | promise(.failure(error! as NSError)) 231 | } 232 | } catch { 233 | print(": \(error)") 234 | promise(.failure(NSError(domain: "json error", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error parsing JSON"]))) 235 | } 236 | } 237 | 238 | task.resume() 239 | }.eraseToAnyPublisher() 240 | } 241 | 242 | static func cutSelectionAreaImage() -> AnyPublisher { 243 | return Future { promise in 244 | guard let displays = ScreenCut.availableContent?.displays else { 245 | promise(.failure(NSError(domain: "display error", code: -1, userInfo: [NSLocalizedDescriptionKey: "没有获取设备"]))) 246 | return 247 | } 248 | let display: SCDisplay = findCurrentScreen( 249 | id: AppDelegate.shared.screentId!, 250 | displays: displays 251 | )! 252 | // print("lt -- displays : \(displays)") 253 | let contentFilter = SCContentFilter(display: display, excludingWindows: []) 254 | let configuration = SCStreamConfiguration() 255 | 256 | // 翻转 Y 坐标 257 | let flippedY = CGFloat(display.height) - ScreenCut.screenArea!.origin.y - ScreenCut.screenArea!.size.height 258 | configuration.sourceRect = CGRectMake( ScreenCut.screenArea!.origin.x, flippedY, ScreenCut.screenArea!.size.width, ScreenCut.screenArea!.size.height) 259 | configuration.destinationRect = CGRectMake( 0, 0, ScreenCut.screenArea!.size.width, ScreenCut.screenArea!.size.height) 260 | configuration.scalesToFit = true 261 | configuration.width = Int(ScreenCut.screenArea!.size.width) 262 | configuration.height = Int(ScreenCut.screenArea!.size.height) 263 | configuration.preservesAspectRatio = true 264 | // print("lt -- source \(configuration.sourceRect) desc:\(configuration.destinationRect)") 265 | 266 | SCScreenshotManager.captureImage(contentFilter: contentFilter, configuration: configuration) { image, error in 267 | // print("lt -- image \(String(describing: image)): eror : %@", error.debugDescription) 268 | 269 | if error == nil && image != nil { 270 | promise(.success(image!)) 271 | } 272 | else { 273 | promise(.failure((error ?? NSError(domain: "capture failure", code: -1, userInfo: [NSLocalizedDescriptionKey: "获取截图数据失败"])) as NSError)) 274 | } 275 | } 276 | }.eraseToAnyPublisher() 277 | } 278 | 279 | // 这个要修改 280 | static func cutImage() { 281 | self.cutSelectionAreaImage().sink { completion in 282 | switch completion { 283 | case .finished: 284 | print("No error.") 285 | case .failure(let error): 286 | print("Error occurred: \(error)") 287 | } 288 | } receiveValue: { cgImage in 289 | self.onlyPasteboardOfSameTime(cgImage) 290 | }.store(in: &cancellables) 291 | } 292 | 293 | static func onlyPasteboardOfSameTime(_ cgImage: CGImage) { 294 | DispatchQueue.main.async { 295 | if UserDefaults.standard.bool(forKey: kplayAudioOfFinished) { 296 | // print("lt -- 播放音效") 297 | NSSound(named: "Ping")?.play() 298 | } 299 | if UserDefaults.standard.bool(forKey: konlySaveInPasteBoard) { 300 | ScreenCut.copyImageToPasteboard(cgImage) 301 | } 302 | else { 303 | ScreenCut.saveImageToFile(cgImage) 304 | ScreenCut.copyImageToPasteboard(cgImage) 305 | } 306 | } 307 | } 308 | } 309 | --------------------------------------------------------------------------------