feat: GiteaHubSync v1.0.0 — fnOS 原生应用:Gitea→GitHub 镜像同步
- 每次推送到飞牛 Gitea(3000 端口)的代码 10 秒内自动镜像到 GitHub 7 个私有仓库 (SMB_Android / DCU_APP / GD32E50X_BOOT / DCU_DesktopController / Motor_v2 / DCU_ECAT_Master / GD32_BOOT),真镜像语义(删分支/标签同步删) - 标准 .fpk 原生应用:不进 Docker,依赖 python312,安装后进入应用中心已安装列表 和桌面启动台,支持启动/停止/重启/卸载,NAS 重启自动恢复 - 控制面板(:8899):自动同步开关、立即同步、仓库状态、同步日志、打开 Gitea - GitHub 推送走 SSH 22 端口直连(HTTPS 443 本地网络被断),部署私钥随应用本地分发 - 附 fnos-install.js(命令行自动安装)、build.sh(重新构建 fpk)、genicon.py(图标生成) 注意:GitHub 部署私钥(GiteaHubSync/app/ssh/id_ed25519)与打包产物 .fpk 按 .gitignore 规则不进仓库,仅保留在工作目录本地分发。
This commit is contained in:
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate GiteaHubSync app icons (pure stdlib, no PIL).
|
||||
Design: rounded gradient tile (blue -> teal) + white double sync arrows."""
|
||||
import os
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "GiteaHubSync")
|
||||
|
||||
|
||||
class Canvas:
|
||||
def __init__(self, w, h, scale=4):
|
||||
self.w, self.h, self.s = w, h, scale
|
||||
self.W, self.H = w * scale, h * scale
|
||||
self.px = [(0, 0, 0, 0)] * (self.W * self.H)
|
||||
|
||||
def _set(self, x, y, rgba):
|
||||
if 0 <= x < self.W and 0 <= y < self.H:
|
||||
i = y * self.W + x
|
||||
r, g, b, a = rgba
|
||||
orr, og, ob, oa = self.px[i]
|
||||
na = a / 255.0 + oa / 255.0 * (1 - a / 255.0)
|
||||
if na == 0:
|
||||
self.px[i] = (0, 0, 0, 0)
|
||||
else:
|
||||
nr = (r * a / 255.0 + orr * oa / 255.0 * (1 - a / 255.0)) / na
|
||||
ng = (g * a / 255.0 + og * ob / 255.0 * (1 - a / 255.0)) / na
|
||||
nb = (b * a / 255.0 + ob * oa / 255.0 * (1 - a / 255.0)) / na
|
||||
self.px[i] = (int(nr), int(ng), int(nb), int(na * 255))
|
||||
|
||||
def line(self, x1, y1, x2, y2, thick, rgba):
|
||||
steps = int(max(abs(x2 - x1), abs(y2 - y1)) * 2) + 1
|
||||
for i in range(steps + 1):
|
||||
t = i / steps
|
||||
cx, cy = x1 + (x2 - x1) * t, y1 + (y2 - y1) * t
|
||||
r = thick / 2.0
|
||||
for yy in range(int(cy - r) - 1, int(cy + r) + 2):
|
||||
for xx in range(int(cx - r) - 1, int(cx + r) + 2):
|
||||
if (xx - cx) ** 2 + (yy - cy) ** 2 <= r * r:
|
||||
self._set(xx, yy, rgba)
|
||||
|
||||
def triangle(self, pts, rgba):
|
||||
ys = [p[1] for p in pts]
|
||||
for y in range(int(min(ys)), int(max(ys)) + 1):
|
||||
xints = []
|
||||
for i in range(3):
|
||||
x1, y1 = pts[i]
|
||||
x2, y2 = pts[(i + 1) % 3]
|
||||
if y1 == y2:
|
||||
continue
|
||||
if min(y1, y2) <= y < max(y1, y2):
|
||||
t = (y - y1) / (y2 - y1)
|
||||
xints.append(x1 + t * (x2 - x1))
|
||||
if len(xints) >= 2:
|
||||
xints.sort()
|
||||
for x in range(int(xints[0]), int(xints[-1]) + 1):
|
||||
self._set(x, y, rgba)
|
||||
|
||||
def gradient_fill(self, x, y, w, h, rad, c1, c2):
|
||||
for yy in range(y, y + h):
|
||||
t = (yy - y) / max(h - 1, 1)
|
||||
r = int(c1[0] + (c2[0] - c1[0]) * t)
|
||||
g = int(c1[1] + (c2[1] - c1[1]) * t)
|
||||
b = int(c1[2] + (c2[2] - c1[2]) * t)
|
||||
for xx in range(x, x + w):
|
||||
dx = max(x + rad - xx, xx - (x + w - 1 - rad), 0)
|
||||
dy = max(y + rad - yy, yy - (y + h - 1 - rad), 0)
|
||||
if dx * dx + dy * dy <= rad * rad:
|
||||
self._set(xx, yy, (r, g, b, 255))
|
||||
|
||||
def save(self, path):
|
||||
s = self.s
|
||||
rows = []
|
||||
for y in range(self.h):
|
||||
row = bytearray([0])
|
||||
for x in range(self.w):
|
||||
rs = gs = bs = as_ = 0
|
||||
cnt = 0
|
||||
for dy in range(s):
|
||||
for dx in range(s):
|
||||
p = self.px[(y * s + dy) * self.W + (x * s + dx)]
|
||||
rs += p[0]; gs += p[1]; bs += p[2]; as_ += p[3]
|
||||
cnt += 1
|
||||
row += bytes((rs // cnt, gs // cnt, bs // cnt, as_ // cnt))
|
||||
rows.append(bytes(row))
|
||||
raw = b"".join(rows)
|
||||
|
||||
def chunk(tag, data):
|
||||
c = struct.pack(">I", len(data)) + tag + data
|
||||
return c + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
|
||||
png = b"\x89PNG\r\n\x1a\n"
|
||||
png += chunk(b"IHDR", struct.pack(">IIBBBBB", self.w, self.h, 8, 6, 0, 0, 0))
|
||||
png += chunk(b"IDAT", zlib.compress(raw, 9))
|
||||
png += chunk(b"IEND", b"")
|
||||
with open(path, "wb") as f:
|
||||
f.write(png)
|
||||
|
||||
|
||||
def draw_icon(size):
|
||||
c = Canvas(size, size)
|
||||
S = c.s
|
||||
W = c.W
|
||||
m = int(size * 0.05) * S
|
||||
# 渐变圆角瓷砖:蓝 -> 青
|
||||
c.gradient_fill(m, m, W - 2 * m, c.H - 2 * m, int(size * 0.22) * S,
|
||||
(37, 99, 235), (13, 148, 136))
|
||||
white = (255, 255, 255, 255)
|
||||
t = int(size * 0.055) * S # 箭杆粗细
|
||||
ah = int(size * 0.11) * S # 箭头大小
|
||||
y1 = int(size * 0.42) * S # 上箭头中心线
|
||||
y2 = int(size * 0.58) * S # 下箭头中心线
|
||||
# 上箭头:向左的杆 + 左箭头
|
||||
c.line(W * 0.40, y1, W * 0.68, y1, t, white)
|
||||
c.triangle([(W * 0.70, y1), (W * 0.70 - ah, y1 - ah * 0.62), (W * 0.70 - ah, y1 + ah * 0.62)], white)
|
||||
# 下箭头:向右的杆 + 右箭头
|
||||
c.line(W * 0.32, y2, W * 0.60, y2, t, white)
|
||||
c.triangle([(W * 0.30, y2), (W * 0.30 + ah, y2 - ah * 0.62), (W * 0.30 + ah, y2 + ah * 0.62)], white)
|
||||
return c
|
||||
|
||||
|
||||
def main():
|
||||
for size, rel in [(256, "ICON_256.PNG"), (64, "ICON.PNG")]:
|
||||
draw_icon(size).save(os.path.join(BASE, rel))
|
||||
print("built", rel)
|
||||
img = os.path.join(BASE, "app", "ui", "images")
|
||||
os.makedirs(img, exist_ok=True)
|
||||
for size, name in [(256, "icon_256.png"), (64, "icon_64.png")]:
|
||||
draw_icon(size).save(os.path.join(img, name))
|
||||
print("built", name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user