├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------