update at 2026-03-18 13:35:19
This commit is contained in:
@@ -1,166 +0,0 @@
|
||||
#!/usr/bin/env swift
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
struct HandConfig {
|
||||
let sourcePath: String
|
||||
let outputDirectoryName: String
|
||||
let frameCount: Int
|
||||
let sourceWidth: CGFloat
|
||||
let sourceHeight: CGFloat
|
||||
let pivotX: CGFloat
|
||||
let pivotY: CGFloat
|
||||
let digits: Int
|
||||
let angleStep: CGFloat
|
||||
}
|
||||
|
||||
enum AssetError: Error, CustomStringConvertible {
|
||||
case invalidImage(String)
|
||||
case pngEncodingFailed(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .invalidImage(path):
|
||||
return "无法读取图片:\(path)"
|
||||
case let .pngEncodingFailed(path):
|
||||
return "无法编码 PNG:\(path)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let workingDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath, isDirectory: true)
|
||||
let repoRoot = CommandLine.arguments.count > 1
|
||||
? URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
|
||||
: workingDirectory
|
||||
let outputRoot = CommandLine.arguments.count > 2
|
||||
? URL(fileURLWithPath: CommandLine.arguments[2], isDirectory: true)
|
||||
: repoRoot.appendingPathComponent("assets/kindle-clock", isDirectory: true)
|
||||
|
||||
let faceSourceURL = repoRoot.appendingPathComponent("assets/clock-face.png")
|
||||
let targetSize = NSSize(width: 220, height: 220)
|
||||
let faceSourceSize = NSSize(width: 431, height: 431)
|
||||
let scale = targetSize.width / faceSourceSize.width
|
||||
|
||||
let handConfigs = [
|
||||
HandConfig(
|
||||
sourcePath: "assets/hour-hand.png",
|
||||
outputDirectoryName: "hour-hand",
|
||||
frameCount: 720,
|
||||
sourceWidth: 32,
|
||||
sourceHeight: 205,
|
||||
pivotX: 13,
|
||||
pivotY: 138,
|
||||
digits: 3,
|
||||
angleStep: 0.5
|
||||
),
|
||||
HandConfig(
|
||||
sourcePath: "assets/minite-hand.png",
|
||||
outputDirectoryName: "minute-hand",
|
||||
frameCount: 60,
|
||||
sourceWidth: 32,
|
||||
sourceHeight: 288,
|
||||
pivotX: 15,
|
||||
pivotY: 203,
|
||||
digits: 2,
|
||||
angleStep: 6
|
||||
),
|
||||
]
|
||||
|
||||
func loadImage(at url: URL) throws -> NSImage {
|
||||
guard let image = NSImage(contentsOf: url) else {
|
||||
throw AssetError.invalidImage(url.path)
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
func savePNG(_ image: NSImage, to url: URL) throws {
|
||||
guard
|
||||
let tiffRepresentation = image.tiffRepresentation,
|
||||
let bitmap = NSBitmapImageRep(data: tiffRepresentation),
|
||||
let pngData = bitmap.representation(using: .png, properties: [:])
|
||||
else {
|
||||
throw AssetError.pngEncodingFailed(url.path)
|
||||
}
|
||||
|
||||
try fileManager.createDirectory(
|
||||
at: url.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
try pngData.write(to: url)
|
||||
}
|
||||
|
||||
func renderImage(size: NSSize, draw: () -> Void) -> NSImage {
|
||||
let image = NSImage(size: size)
|
||||
image.lockFocusFlipped(true)
|
||||
NSColor.clear.setFill()
|
||||
NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
|
||||
draw()
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}
|
||||
|
||||
func renderFace() throws {
|
||||
let faceImage = try loadImage(at: faceSourceURL)
|
||||
let renderedFace = renderImage(size: targetSize) {
|
||||
faceImage.draw(
|
||||
in: NSRect(origin: .zero, size: targetSize),
|
||||
from: NSRect(origin: .zero, size: faceImage.size),
|
||||
operation: .sourceOver,
|
||||
fraction: 1
|
||||
)
|
||||
}
|
||||
|
||||
try savePNG(renderedFace, to: outputRoot.appendingPathComponent("clock-face.png"))
|
||||
}
|
||||
|
||||
func renderHandFrames(config: HandConfig) throws {
|
||||
let sourceURL = repoRoot.appendingPathComponent(config.sourcePath)
|
||||
let handImage = try loadImage(at: sourceURL)
|
||||
let outputDirectory = outputRoot.appendingPathComponent(config.outputDirectoryName, isDirectory: true)
|
||||
let scaledWidth = config.sourceWidth * scale
|
||||
let scaledHeight = config.sourceHeight * scale
|
||||
let scaledPivotX = config.pivotX * scale
|
||||
let scaledPivotY = config.pivotY * scale
|
||||
|
||||
try fileManager.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||
|
||||
for frameIndex in 0..<config.frameCount {
|
||||
let angle = CGFloat(frameIndex) * config.angleStep
|
||||
let renderedFrame = renderImage(size: targetSize) {
|
||||
guard let context = NSGraphicsContext.current?.cgContext else {
|
||||
return
|
||||
}
|
||||
|
||||
context.translateBy(x: targetSize.width / 2, y: targetSize.height / 2)
|
||||
context.rotate(by: angle * .pi / 180)
|
||||
context.translateBy(x: -scaledPivotX, y: -scaledPivotY)
|
||||
|
||||
handImage.draw(
|
||||
in: NSRect(x: 0, y: 0, width: scaledWidth, height: scaledHeight),
|
||||
from: NSRect(origin: .zero, size: handImage.size),
|
||||
operation: .sourceOver,
|
||||
fraction: 1
|
||||
)
|
||||
}
|
||||
|
||||
let filename = String(format: "%0\(config.digits)d.png", frameIndex)
|
||||
try savePNG(renderedFrame, to: outputDirectory.appendingPathComponent(filename))
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try fileManager.createDirectory(at: outputRoot, withIntermediateDirectories: true)
|
||||
try renderFace()
|
||||
|
||||
for config in handConfigs {
|
||||
try renderHandFrames(config: config)
|
||||
}
|
||||
|
||||
print("Generated Kindle clock assets at \(outputRoot.path)")
|
||||
} catch {
|
||||
fputs("\(error)\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
# 强制清理残留 SSH 进程,然后在 22 端口拉起一份 usbnet 自带的 OpenSSH。
|
||||
# 这份 sshd 会优先读取 /mnt/us/usbnet/etc/dot.ssh/authorized_keys。
|
||||
|
||||
TS="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo now)"
|
||||
OUT_DIR="/mnt/us/ssh-debug/${TS}"
|
||||
LOG_FILE="${OUT_DIR}/force-openssh-22.log"
|
||||
PID_FILE="/mnt/us/usbnet/run/sshd-force-22.pid"
|
||||
SOURCE_KEYS="/mnt/us/usbnet/etc/authorized_keys"
|
||||
TARGET_KEYS="/mnt/us/usbnet/etc/dot.ssh/authorized_keys"
|
||||
|
||||
mkdir -p "${OUT_DIR}" /mnt/us/usbnet/run /mnt/us/usbnet/etc/dot.ssh
|
||||
exec >"${LOG_FILE}" 2>&1
|
||||
|
||||
echo "=== FORCE OPENSSH 22 ==="
|
||||
date 2>/dev/null || true
|
||||
id 2>/dev/null || true
|
||||
|
||||
if [ -f "${SOURCE_KEYS}" ]; then
|
||||
cp "${SOURCE_KEYS}" "${TARGET_KEYS}"
|
||||
chmod 600 "${TARGET_KEYS}" 2>/dev/null || true
|
||||
fi
|
||||
chmod 755 /mnt/us/usbnet/etc/dot.ssh 2>/dev/null || true
|
||||
|
||||
killall sshd 2>/dev/null || true
|
||||
killall dropbear 2>/dev/null || true
|
||||
killall dropbearmulti 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
rm -f "${PID_FILE}" 2>/dev/null || true
|
||||
iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT 2>/dev/null || true
|
||||
|
||||
(
|
||||
exec /mnt/us/usbnet/sbin/sshd -D -e \
|
||||
-f /mnt/us/usbnet/etc/sshd_config \
|
||||
-o ListenAddress=0.0.0.0 \
|
||||
-o Port=22 \
|
||||
-o PidFile="${PID_FILE}" \
|
||||
-o AuthorizedKeysFile="${TARGET_KEYS}" \
|
||||
-o PasswordAuthentication=no \
|
||||
-o KbdInteractiveAuthentication=no \
|
||||
-o PubkeyAuthentication=yes \
|
||||
-o PermitRootLogin=yes \
|
||||
-o HostKey=/mnt/us/usbnet/etc/ssh_host_rsa_key \
|
||||
-o HostKey=/mnt/us/usbnet/etc/ssh_host_ecdsa_key \
|
||||
-o HostKey=/mnt/us/usbnet/etc/ssh_host_ed25519_key
|
||||
) &
|
||||
|
||||
LAUNCHER_PID="$!"
|
||||
echo "${LAUNCHER_PID}" > "${OUT_DIR}/launcher.pid"
|
||||
sleep 1
|
||||
|
||||
echo "launcher pid: ${LAUNCHER_PID}"
|
||||
echo "pid file: ${PID_FILE}"
|
||||
if [ -x /mnt/us/usbnet/bin/lsof ]; then
|
||||
/mnt/us/usbnet/bin/lsof -n -P -iTCP:22 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "=== DONE ==="
|
||||
echo "${OUT_DIR}"
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
TMP_DIR="${TMPDIR:-/tmp}/kindle-clock-assets"
|
||||
KINDLE_TARGET=${1:-kindle}
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
/usr/bin/swift "$ROOT_DIR/scripts/generate-kindle-clock-assets.swift" "$ROOT_DIR" "$TMP_DIR"
|
||||
|
||||
ssh "$KINDLE_TARGET" 'mkdir -p /mnt/us/dashboard/assets/hour-hand /mnt/us/dashboard/assets/minute-hand'
|
||||
rsync -av --no-o --no-g --delete "$TMP_DIR"/ "$KINDLE_TARGET":/mnt/us/dashboard/assets/
|
||||
|
||||
echo "Clock assets synced to $KINDLE_TARGET:/mnt/us/dashboard/assets"
|
||||
@@ -88,6 +88,9 @@ sync_dashboard_runtime() {
|
||||
rsync -av --no-o --no-g \
|
||||
"$ROOT_DIR/dash/src/start.sh" \
|
||||
"$ROOT_DIR/dash/src/dash.sh" \
|
||||
"$ROOT_DIR/dash/src/stop.sh" \
|
||||
"$ROOT_DIR/dash/src/launch-from-kual.sh" \
|
||||
"$ROOT_DIR/dash/src/launch-theme-from-kual.sh" \
|
||||
"$ROOT_DIR/dash/src/switch-theme.sh" \
|
||||
"$KINDLE_TARGET":/mnt/us/dashboard/
|
||||
|
||||
@@ -97,16 +100,24 @@ sync_dashboard_runtime() {
|
||||
"$ROOT_DIR/dash/src/local/clock-index.sh" \
|
||||
"$ROOT_DIR/dash/src/local/render-clock.lua" \
|
||||
"$ROOT_DIR/dash/src/local/render-clock.sh" \
|
||||
"$ROOT_DIR/dash/src/local/touch-home-service.sh" \
|
||||
"$ROOT_DIR/dash/src/local/theme-menu-service.sh" \
|
||||
"$ROOT_DIR/dash/src/local/theme-json.lua" \
|
||||
"$ROOT_DIR/dash/src/local/theme-sync.sh" \
|
||||
"$KINDLE_TARGET":/mnt/us/dashboard/local/
|
||||
|
||||
ssh "$KINDLE_TARGET" "chmod +x /mnt/us/dashboard/start.sh /mnt/us/dashboard/dash.sh /mnt/us/dashboard/switch-theme.sh /mnt/us/dashboard/local/fetch-dashboard.sh /mnt/us/dashboard/local/clock-index.sh /mnt/us/dashboard/local/render-clock.sh /mnt/us/dashboard/local/theme-menu-service.sh /mnt/us/dashboard/local/theme-sync.sh"
|
||||
ssh "$KINDLE_TARGET" "chmod +x /mnt/us/dashboard/start.sh /mnt/us/dashboard/dash.sh /mnt/us/dashboard/stop.sh /mnt/us/dashboard/launch-from-kual.sh /mnt/us/dashboard/launch-theme-from-kual.sh /mnt/us/dashboard/switch-theme.sh /mnt/us/dashboard/local/fetch-dashboard.sh /mnt/us/dashboard/local/clock-index.sh /mnt/us/dashboard/local/render-clock.sh /mnt/us/dashboard/local/touch-home-service.sh /mnt/us/dashboard/local/theme-menu-service.sh /mnt/us/dashboard/local/theme-sync.sh"
|
||||
ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/dashboard/local/state"
|
||||
ssh "$KINDLE_TARGET" "date '+%s' >/mnt/us/dashboard/local/state/background-updated-at"
|
||||
}
|
||||
|
||||
sync_kual_extension() {
|
||||
ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/extensions/kindle-dash"
|
||||
rsync -av --no-o --no-g \
|
||||
"$ROOT_DIR/dash/KUAL/kindle-dash/" \
|
||||
"$KINDLE_TARGET":/mnt/us/extensions/kindle-dash/
|
||||
}
|
||||
|
||||
sync_theme_bundle() {
|
||||
rsync -av --no-o --no-g \
|
||||
"$ROOT_DIR/calendar/dist/themes.json" \
|
||||
@@ -175,6 +186,7 @@ update_default_clock_region_env() {
|
||||
|
||||
sync_dashboard_runtime
|
||||
sync_theme_bundle
|
||||
sync_kual_extension
|
||||
update_default_clock_region_env
|
||||
|
||||
if [ -n "$THEME_FILTER" ]; then
|
||||
|
||||
Reference in New Issue
Block a user