feat: add dictionary hotwords and insights UI

This commit is contained in:
baiqing committed 2026-04-27 13:53:18 +08:00
commit f5a0c810fa
70 files changed
+9089

No files matched your search

+90
View File
@@ -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)开始录音;再按一次结束;语音文字会插入当前输入框"
+131
View File
@@ -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)")