feat: add dictionary hotwords and insights UI
This commit is contained in:
commit
f5a0c810fa
70 files changed
+9089
No files matched your search
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# 把 OpenLess 打包成 .app bundle + ad-hoc codesign
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
APP_NAME="OpenLess"
|
||||
BUNDLE_ID="com.openless.app"
|
||||
APP_VERSION="0.1.8"
|
||||
BUILD_NUMBER="9"
|
||||
BUILD_DIR="build"
|
||||
APP_DIR="${BUILD_DIR}/${APP_NAME}.app"
|
||||
BIN_DIR="${APP_DIR}/Contents/MacOS"
|
||||
RES_DIR="${APP_DIR}/Contents/Resources"
|
||||
|
||||
echo "[build-app] generate app icon"
|
||||
swift scripts/generate-app-icon.swift
|
||||
|
||||
echo "[build-app] swift build -c release"
|
||||
swift build -c release --product "${APP_NAME}"
|
||||
|
||||
BIN_SRC=".build/release/${APP_NAME}"
|
||||
[ -f "${BIN_SRC}" ] || { echo "missing ${BIN_SRC}"; exit 1; }
|
||||
|
||||
echo "[build-app] assemble bundle at ${APP_DIR}"
|
||||
rm -rf "${APP_DIR}"
|
||||
mkdir -p "${BIN_DIR}" "${RES_DIR}"
|
||||
cp "${BIN_SRC}" "${BIN_DIR}/${APP_NAME}"
|
||||
cp "Resources/AppIcon.icns" "${RES_DIR}/AppIcon.icns"
|
||||
|
||||
cat > "${APP_DIR}/Contents/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${APP_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${BUNDLE_ID}</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${APP_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${APP_VERSION}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${BUILD_NUMBER}</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>15.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>OpenLess 需要麦克风权限以录制您的语音并转写为文字。</string>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>OpenLess 需要权限以将转写后的文字插入到您当前光标所在的输入框。</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "[build-app] ad-hoc code sign"
|
||||
codesign --force --deep --sign - "${APP_DIR}"
|
||||
|
||||
echo "[build-app] kill old app"
|
||||
killall "${APP_NAME}" 2>/dev/null || true
|
||||
if [[ "${RESET_TCC:-1}" != "0" ]]; then
|
||||
echo "[build-app] reset TCC approvals"
|
||||
tccutil reset Accessibility "${BUNDLE_ID}" 2>/dev/null || true
|
||||
tccutil reset Microphone "${BUNDLE_ID}" 2>/dev/null || true
|
||||
tccutil reset AppleEvents "${BUNDLE_ID}" 2>/dev/null || true
|
||||
tccutil reset ListenEvent "${BUNDLE_ID}" 2>/dev/null || true
|
||||
else
|
||||
echo "[build-app] keep existing TCC approvals"
|
||||
fi
|
||||
|
||||
echo "[build-app] done: ${APP_DIR}"
|
||||
echo
|
||||
echo "下一步:"
|
||||
echo " 1. open ${APP_DIR} # 双击启动;默认已重置权限,会重新弹辅助功能 + 麦克风权限"
|
||||
echo " 2. 系统设置 → 隐私与安全 → 辅助功能 / 麦克风 → 勾选 OpenLess"
|
||||
echo " 3. 屏幕右上角菜单栏 OpenLess 图标 → 退出 OpenLess(必须先退出再重启)"
|
||||
echo " 4. 再次 open ${APP_DIR}"
|
||||
echo " 5. 在菜单栏「打开设置…」填火山 APP ID、Access Token、Resource ID,以及 Ark API Key(保存到 ~/.openless/credentials.json,0600)"
|
||||
echo " 6. 实时日志(另开终端): tail -f ~/Library/Logs/OpenLess/OpenLess.log"
|
||||
echo " 7. 按右 Option(或 Fn)开始录音;再按一次结束;语音文字会插入当前输入框"
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env swift
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let root = URL(fileURLWithPath: fileManager.currentDirectoryPath, isDirectory: true)
|
||||
|
||||
let sourceURL: URL
|
||||
if CommandLine.arguments.count > 1 {
|
||||
sourceURL = URL(fileURLWithPath: CommandLine.arguments[1])
|
||||
} else {
|
||||
sourceURL = root.appendingPathComponent("Resources/Brand/openless-app-icon-source.jpg")
|
||||
}
|
||||
|
||||
let outputDir = root.appendingPathComponent("Resources", isDirectory: true)
|
||||
let previewURL = outputDir.appendingPathComponent("AppIcon.png")
|
||||
let standardImageURL = outputDir.appendingPathComponent("Brand/openless-standard-image.png")
|
||||
let iconsetURL = root.appendingPathComponent(".build/AppIcon.iconset", isDirectory: true)
|
||||
let icnsURL = outputDir.appendingPathComponent("AppIcon.icns")
|
||||
|
||||
guard let source = NSImage(contentsOf: sourceURL),
|
||||
let cgImage = source.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
||||
fputs("[icon] failed to load \(sourceURL.path)\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
try fileManager.createDirectory(at: outputDir, withIntermediateDirectories: true)
|
||||
try fileManager.createDirectory(at: standardImageURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
try? fileManager.removeItem(at: iconsetURL)
|
||||
try fileManager.createDirectory(at: iconsetURL, withIntermediateDirectories: true)
|
||||
|
||||
// Normalize any square-ish brand source into a centered symbol icon. The current
|
||||
// source is already a cropped symbol, but the center crop keeps future exports safe.
|
||||
let sourceWidth = CGFloat(cgImage.width)
|
||||
let sourceHeight = CGFloat(cgImage.height)
|
||||
let cropSide = min(sourceWidth, sourceHeight)
|
||||
let cropRect = CGRect(
|
||||
x: (sourceWidth - cropSide) / 2,
|
||||
y: (sourceHeight - cropSide) / 2,
|
||||
width: cropSide,
|
||||
height: cropSide
|
||||
)
|
||||
guard let cropped = cgImage.cropping(to: cropRect) else {
|
||||
fputs("[icon] failed to crop source image\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
func renderedPNG(size: Int) throws -> Data {
|
||||
guard let rep = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil,
|
||||
pixelsWide: size,
|
||||
pixelsHigh: size,
|
||||
bitsPerSample: 8,
|
||||
samplesPerPixel: 4,
|
||||
hasAlpha: true,
|
||||
isPlanar: false,
|
||||
colorSpaceName: .deviceRGB,
|
||||
bytesPerRow: 0,
|
||||
bitsPerPixel: 0
|
||||
) else {
|
||||
throw NSError(domain: "OpenLessIcon", code: 1, userInfo: [NSLocalizedDescriptionKey: "bitmap allocation failed"])
|
||||
}
|
||||
|
||||
rep.size = NSSize(width: size, height: size)
|
||||
guard let context = NSGraphicsContext(bitmapImageRep: rep) else {
|
||||
throw NSError(domain: "OpenLessIcon", code: 2, userInfo: [NSLocalizedDescriptionKey: "graphics context allocation failed"])
|
||||
}
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = context
|
||||
context.imageInterpolation = .high
|
||||
|
||||
let rect = NSRect(x: 0, y: 0, width: size, height: size)
|
||||
NSColor.clear.setFill()
|
||||
rect.fill()
|
||||
|
||||
let radius = CGFloat(size) * 0.22
|
||||
let mask = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
|
||||
mask.addClip()
|
||||
|
||||
NSColor(calibratedRed: 0.985, green: 0.985, blue: 0.98, alpha: 1).setFill()
|
||||
rect.fill()
|
||||
|
||||
let image = NSImage(cgImage: cropped, size: NSSize(width: cropped.width, height: cropped.height))
|
||||
let inset = CGFloat(size) * 0.045
|
||||
let imageRect = rect.insetBy(dx: inset, dy: inset)
|
||||
image.draw(in: imageRect, from: NSRect(x: 0, y: 0, width: cropped.width, height: cropped.height), operation: .sourceOver, fraction: 1)
|
||||
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
guard let data = rep.representation(using: .png, properties: [:]) else {
|
||||
throw NSError(domain: "OpenLessIcon", code: 3, userInfo: [NSLocalizedDescriptionKey: "png encoding failed"])
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
let iconFiles: [(name: String, size: Int)] = [
|
||||
("icon_16x16.png", 16),
|
||||
("icon_16x16@2x.png", 32),
|
||||
("icon_32x32.png", 32),
|
||||
("icon_32x32@2x.png", 64),
|
||||
("icon_128x128.png", 128),
|
||||
("icon_128x128@2x.png", 256),
|
||||
("icon_256x256.png", 256),
|
||||
("icon_256x256@2x.png", 512),
|
||||
("icon_512x512.png", 512),
|
||||
("icon_512x512@2x.png", 1024),
|
||||
]
|
||||
|
||||
let standardImage = try renderedPNG(size: 1024)
|
||||
try standardImage.write(to: previewURL, options: .atomic)
|
||||
try standardImage.write(to: standardImageURL, options: .atomic)
|
||||
for file in iconFiles {
|
||||
let url = iconsetURL.appendingPathComponent(file.name)
|
||||
try renderedPNG(size: file.size).write(to: url, options: .atomic)
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil")
|
||||
process.arguments = ["-c", "icns", iconsetURL.path, "-o", icnsURL.path]
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
guard process.terminationStatus == 0 else {
|
||||
fputs("[icon] iconutil failed with status \(process.terminationStatus)\n", stderr)
|
||||
exit(process.terminationStatus)
|
||||
}
|
||||
|
||||
print("[icon] wrote \(previewURL.path)")
|
||||
print("[icon] wrote \(standardImageURL.path)")
|
||||
print("[icon] wrote \(icnsURL.path)")
|
||||
Reference in new issue
Block a user