Skip to content

Releases: Buwrt/githup

githup v1.2.18

Choose a tag to compare

@Buwrt Buwrt released this 25 Oct 18:24

githup v1.2.16(Actions 日志:App 内直接看)

这一版做了什么:Actions 日志

以前在 Actions 里点「查看日志」,其实是被丢去了浏览器 ——
App 里那个按钮是个假动作。

原因不复杂:/actions/runs/{id}/logs 不是直接吐日志,
而是 302 跳到一段带签名的下载地址。前端既过不去跨域,
又不能在跳转后把令牌摘掉(带着令牌反而会被判成多余凭据,直接 403)。
两件事凑一起,就只有原生层能同时满足:带着令牌去要,跳走之后摘掉令牌再取。

这一版把这条路打通了:原生层把日志包取回、解压成文本交给前端,
前端按官网的样子呈现出来。

  • 按步骤分栏:日志包里一个文件就是一个步骤,顶部横滑切换
  • 还原 ANSI 颜色:红是报错、黄是警告、绿是通过,跟你在网页上看到的一样
  • 关键字过滤:输入即筛,命中的行标黄,并告诉你命中了几行
  • 一键复制:整段日志复制走,方便贴给人看
  • 取不到时说人话:日志只在 GitHub 上保留一段时间,
    过期或被清理时会明确提示,并给「在浏览器查看」兜底,不会把人困在空面板里

每个作业单独有一个「查看日志」(只看这一个作业),
右上角菜单里另有「查看完整日志」(整次运行),与官网两个入口对齐。

安装信息

项 值
大小 1,295,257 字节(约 1265 KB)
SHA-256 66574f3378a6f0b58d1a2d09e88b0b7e175deed4c8da0a752e925916a488a1b0



githup v1.2.17

Choose a tag to compare

@Buwrt Buwrt released this 25 Oct 18:05

githup v1.2.16(Actions 日志:App 内直接看)

这一版做了什么:Actions 日志

以前在 Actions 里点「查看日志」,其实是被丢去了浏览器 ——
App 里那个按钮是个假动作。

原因不复杂:/actions/runs/{id}/logs 不是直接吐日志,
而是 302 跳到一段带签名的下载地址。前端既过不去跨域,
又不能在跳转后把令牌摘掉(带着令牌反而会被判成多余凭据,直接 403)。
两件事凑一起,就只有原生层能同时满足:带着令牌去要,跳走之后摘掉令牌再取。

这一版把这条路打通了:原生层把日志包取回、解压成文本交给前端,
前端按官网的样子呈现出来。

  • 按步骤分栏:日志包里一个文件就是一个步骤,顶部横滑切换
  • 还原 ANSI 颜色:红是报错、黄是警告、绿是通过,跟你在网页上看到的一样
  • 关键字过滤:输入即筛,命中的行标黄,并告诉你命中了几行
  • 一键复制:整段日志复制走,方便贴给人看
  • 取不到时说人话:日志只在 GitHub 上保留一段时间,
    过期或被清理时会明确提示,并给「在浏览器查看」兜底,不会把人困在空面板里

每个作业单独有一个「查看日志」(只看这一个作业),
右上角菜单里另有「查看完整日志」(整次运行),与官网两个入口对齐。

安装信息

项 值
大小 1,295,257 字节(约 1265 KB)
SHA-256 66574f3378a6f0b58d1a2d09e88b0b7e175deed4c8da0a752e925916a488a1b0



githup v1.2.16

Choose a tag to compare

@Buwrt Buwrt released this 16 Oct 06:06
0b06522

githup v1.2.16(Actions 日志:App 内直接看)

这一版做了什么:Actions 日志

以前在 Actions 里点「查看日志」,其实是被丢去了浏览器 ——
App 里那个按钮是个假动作。

原因不复杂:/actions/runs/{id}/logs 不是直接吐日志,
而是 302 跳到一段带签名的下载地址。前端既过不去跨域,
又不能在跳转后把令牌摘掉(带着令牌反而会被判成多余凭据,直接 403)。
两件事凑一起,就只有原生层能同时满足:带着令牌去要,跳走之后摘掉令牌再取。

这一版把这条路打通了:原生层把日志包取回、解压成文本交给前端,
前端按官网的样子呈现出来。

  • 按步骤分栏:日志包里一个文件就是一个步骤,顶部横滑切换
  • 还原 ANSI 颜色:红是报错、黄是警告、绿是通过,跟你在网页上看到的一样
  • 关键字过滤:输入即筛,命中的行标黄,并告诉你命中了几行
  • 一键复制:整段日志复制走,方便贴给人看
  • 取不到时说人话:日志只在 GitHub 上保留一段时间,
    过期或被清理时会明确提示,并给「在浏览器查看」兜底,不会把人困在空面板里

每个作业单独有一个「查看日志」(只看这一个作业),
右上角菜单里另有「查看完整日志」(整次运行),与官网两个入口对齐。

安装信息

项 值
大小 1,109,487 字节(约 1083 KB)
SHA-256 1ef6b291942ac9320a2ae96b0e5d4545bc738f5cf536c9ca1e7a74bec0738d51

这一版还做了什么:探索页换成官方趋势榜

「今日 / 本周 / 本月点来点去,内容都不变」—— 这不是刷新慢,是口径错了。

以前的趋势榜走搜索接口 created:>某日 sort=stars,排的是 star 总量。
总量是缓慢累积的量,排序天生就稳:实测 7 天窗口往后滑一天,top10 里
仍有 7 条不动。加上顶部「热门仓库」那 5 条写死成 stars:>50000,
既不认时间范围也不认语言,怎么看都是同一张脸。

官方 Trending 排的是窗口内 star 增量,GitHub 从没给过公开 API,
只有 github.com/trending 这一个入口 —— 这一版直接抓它,和网页版同一口径:

  • 今日 / 本周 / 本月 现在真的对应官方的 daily / weekly / monthly
  • 每条直接写出增量:+2,716 今日,就是官网上那句 "2,716 stars today"
  • 语言筛选照旧:Python / Rust / Kotlin… 切过去就是官方对应语言的榜
  • 抓不到会兜底:退回原来的搜索榜,并在顶部写明「这是按 star 总量排的兜底榜」

那条永远不变的「热门仓库」删掉了。

githup v1.2.15

Choose a tag to compare

@Buwrt Buwrt released this 01 Oct 05:24

githup v1.2.15(versionCode 1002015)

打包成功却看不到 APK —— 硬断言产物存在 + 空态给出原因。


实测数据

githup 库最近 40 次运行里,official-release 跑了 37 次,
其中 8 次「conclusion = success 但 artifacts = 0」
(#100 #97 #96 #86 #83 #82 #78 #73)。

绿勾出现了 8 次,下面一个 APK 都没有。

根因一:continue-on-error 让失败伪装成成功

- name: Build Release APK
  id: b
  continue-on-error: true      # ← 为的是失败时仍能提交日志

gradlew 失败时这条被"容错",整个 job 依然是绿色对勾。
用户看到「打包成功」,点进去却没有 APK。

根因二:if-no-files-found: warn

- name: Upload artifact
  with:
    if-no-files-found: warn    # ← 匹配不到文件也只是警告

产物路径匹配不到时步骤照样绿,又一次"成功但没东西"。

根因三:App 里没产物时整块消失

function artifactsBlock(artifacts) {
  if (!artifacts.length) return '';   // ← 页面一片空白
}

看的人只会以为"加载坏了",想不到是这次运行压根没上传产物。

而且 artifacts 请求失败时 .catch 静默返回空数组 ——
「确实没有」和「没查到」混为一谈,提示必然是错的。

改法

  1. 新增 Assert APK produced(硬断言)

    • 构建退出码非 0 → exit 1
    • 找不到 APK → 打印目录树 + exit 1
    • 小于 300KB → 判为残缺 + exit 1

    绿色从此只代表一件事:确实产出了 APK。

  2. Upload artifact 收紧

    • if-no-files-found: warn → error
    • retention-days: 90 显式写死(不依赖可被改动的仓库默认值)
  3. 新增 Write APK summary
    把文件名 / 大小 / sha256 / 证书指纹写进 Job Summary,
    点开运行一眼可见,不用翻日志。

  4. App 侧空态给出原因(page-detail.js)

    • 没产物 → 说明「只有用了 upload-artifact 才会出现;
      提交到分支或发到 Release 不会显示」
    • 请求失败 → 单独提示「没查到产物列表(请求失败)」

    两块不再混为一谈。

为什么有些运行永远不会出现产物

比如那种只有 Download + Commit 到分支的工作流 ——
它没有 upload-artifact。GitHub 只在用了 upload-artifact 时才
生成产物区。以前这种运行在 App 里就是一片空白;现在会明说原因。


校验信息

项目 类型 完整值
源码指纹 源码快照 SHA-256 c313a6029e0ab66290cb20339a0af4517f876101b7f333a29c8e8fa379e26026
签名指纹 证书 SHA-256 863dd1cd3752e59165e152bc4ab35fde478fcd296d724a6bc58cec0368184927
APK 校验值 安装包 SHA-256 667f1506b36619abda4d0f7519b564091bd06c7739c8439f788fba5e7dc459c7
构建提交 Commit SHA 57dcfbc43167d68f318ba5dbc09f7581357bb641

均为完整值,可在表格里整段选中复制,与官方公布的比对一致再安装。

githup v1.2.14

Choose a tag to compare

@Buwrt Buwrt released this 27 Sep 06:02

githup v1.2.14(翻译速度还原 · 两步验证器:手机上直接出 6 位动态码)

上一版做错了什么

v1.2.13 说「免费引擎从 2 家扩到 5 家」,把 DeepL / Google / 设备端
直接放进了协作池。实测这条路错得离谱:

手动指定「只用有道」翻同一页 —— v1.2.11 是 624ms,v1.2.13 是 9633ms,慢了 15 倍。

有道本身一点没变慢,是它的活被连不上的引擎抢走了:有道只分到 1 批,
DeepL 被派了 6 次、Google 4 次、设备端 2 次,页面在等那些根本连不上的请求。

当初设想的三重措施全没兜住:

措施 为什么没用
3 秒闸门 把单次从 8 秒砍到 3 秒,但它们被派了十几次,接力下来整页照样 9.6 秒
权重垫后 只相对于有道一家垫后,转轮里「所有慢引擎加起来」占了三分之二的位子
连续失败摘牌 要连续失败 3 次才摘,前 3 批的学费已经交完了

闸门治的是「单次等多久」,治不了「反复派给死引擎」。

这一版改成了什么

先验证,再准入 —— 能不能用,探一次说了算。

  • 探不过 → 压根不进池。国内用户永远走有道 + MyMemory 那条快车道
  • 探过了 → 才准进池。此时它是真能干活的,不是来占位置的

探测在后台跑,不占首屏时间;结果记下来,30 分钟内不再重探
(换 WiFi / 开代理 / 出国之后会自动重探一次)。

场景(翻同一页 120 段) v1.2.11 v1.2.13 v1.2.14
手动指定「有道」 624ms 9633ms 624ms
手动指定「MyMemory」 612ms 9621ms 614ms
自动(多引擎协作) 608ms 9612ms 607ms

同一版里还有:看代码 / 改代码变成编辑器的样子(PR #12,来自 @mymine)

  1. 长行不用左右拖了 —— 代码默认自动折行,顶上可以切回「不换行」
  2. 换行时行号不错位 —— 原来行号是单独一列,代码一折行就对不上;改成按行渲染后,
    行号钉在每个逻辑行的开头
  3. 编辑文件时也有行号和语法高亮了 —— 原来是个光秃秃的输入框

「免费引擎变多」没有落空

它变成了「能用的才算多」:海外能通的环境、或者你挂了代理,
那几家探测一过就自动加入,和 1.2.11 里的有道 / MyMemory 一样是真在干活的引擎。
而国内用户不会再为这个「多」付 15 倍的代价。

顺带一提:首屏竞速(v1.2.13 的另一项改动)经查是无辜的 ——
纯竞速版实测 624ms,和 v1.2.11 一模一样。这一版没动它。

MyMemory 配额用完之后,不再每页都白撞一次

它的免费配额是按天算的,一天之内不会自己恢复。以前撞上配额只有
「连续失败 3 次就摘牌」管着,而那本账每次换页就清零重来 ——
于是配额早就用完时,每翻一页都要先白撞 3 批(每批几十条请求)
才又一次得出「它今天不行」。看到的就是每页都莫名慢一截。

现在记一笔账:确认耗尽后 6 小时内不让它自动进池,连那 3 批学费都省了。
取 6 小时而不是一整天,是因为它的配额按 UTC 日重置 ——
半天给一次机会,跨过零点自动回来。

顺带补了一个漏:配额打满时它也可能直接回 429(而不是把警告塞进译文里),
以前只认警告那一种,429 那一路会落回「每页白撞 3 批」的老路。

手动点名要用它时不受影响 —— 你在菜单里点了它,那就得上。

同一版里还有:议题里的图片,加载得跟 README 一样快

同一个 App,README 里的图划到就出来,议题(issue / PR)里贴的截图却要等一下。

根子不在网络,在预热名单漏了它们。README 的图渲染时会被丢给原生后台下载到磁盘,
划到时是读磁盘、零网络;议题里贴的图是 GitHub 附件链接,URL 不带扩展名,
于是既被当成 <video> 渲染、也进不了只认扩展名的预热名单 —— 每次都要现拉。

现在:GitHub 附件链接也进预热名单(最多 3 个、单张超过 4MB 的不预,
免得为一张大图把后台占满);探测用的 <video> 降级成 <img> 时
直接复用已经下载好的那一份,不再重新下一遍。

一处刻意没改:拦截仍然只接管带 image/* 的请求,<video> 一律放行 ——
视频靠 Range 请求做 seek,被整包 200 接过来会把播放弄坏。

同一版里还有:议题里的图,改成按官网的答案渲染(不再靠猜)

上面那一项治的是「进不了预热名单」,这一项治的是更靠前的根子:App 得猜它是什么。

官网不用猜 —— 它的正文是服务端渲染的。你拖一个文件进议题,GitHub 上传时就记下了
它是什么,渲染正文时直接写出来:

你贴的 官网渲染出来的
截图 <img width="327" height="706" src="…">
录屏 <video src="…">

类型和宽高都写在 HTML 里,浏览器一点都不用猜。

我们以前拿的是 Markdown 源码自己渲染,附件在里面是
github.com/user-attachments/assets/<uuid> —— 没有扩展名,看不出是截图还是录屏。
只能乐观当 <video> 渲染、拉不动再降级成 <img>。代价是:绝大多数其实是截图的
附件都要先白跑一趟视频请求;而且拿不到宽高,图一加载完页面就跳一下。

现在正文改要官方渲染好的那份(接口带 Accept: …full+json,一份请求里 Markdown
和 HTML 都有 —— Markdown 留给「引用 / 编辑」,HTML 用来渲染)。

地址就照官网的样子用:官方正文里给的是
private-user-images.githubusercontent.com/…<uuid>.png?jwt=… —— GitHub 自己的
CDN 域名,字节直出、不用跳。官网的图快,快就快在走的是这条直路,所以这一版
把它原样留着当主路径,只把不带签名的那份(user-attachments/assets/<uuid>)
记在旁边当第二条路 —— 那份要先吃一个 302 才到 S3,多一趟往返、多一次握手。

签名 5 分钟有效带来的两个坑:渲染完立刻预热(趁签名还热),
以及缓存不再按整条 URL 记(见下一节)。

同一版里还有:预热带回来的字节,得存进同一个抽屉

上面两条官方线路是同一份字节,可缓存以前是按整条 URL 做 key 的。带签名的
地址每 5 分钟、每次请求都不一样,于是:

预热按 CDN 地址下好一份 → 加载时签名已经换了 → 缓存里没有 → 又下第二份

预热带回来的那一趟等于白做,用户滑到的每一张都还在现拉。

现在缓存 key 改成认地址里那个 uuid(asset-<uuid>):两条官方地址落到磁盘上的
同一个文件,谁先下好谁说了算,另一个直接命中。下载锁也按 uuid 走,不会一个在
下载、另一个同一时刻也在下。没有 uuid 的地址(README 的 raw / camo / 头像)原样记,
不受影响。

同一版里还有:图片不再被我们「接走」—— 接走的那条路反而更慢

从 v1.2.9 起,图片比更早的版本慢:加的东西越来越多(缓存、预热、快车道),
体感却在往回走。真因不在缓存,在**「接管」这件事本身**。

v1.2.9 之前,图一直是 WebView 自己拉的 —— Chromium 的网络栈,HTTP/2 多路复用、
TLS 会话复用、它自己的 HTTP 缓存(304)、以及边下边渲染。v1.2.9 加了 ImageProxy
之后变成「WebView 要图,我们全接过来自己下」,而一旦被接住,Chromium 就不再发它自己
那趟请求 —— 等于把快车道换成了慢车道(HTTP/1.1、只有 gzip、整包到齐才出第一帧、
还同步占住 WebView 有限的几个 IO 线程,一张慢图把后面排队的图一起拖住)。

现在改成只接得住的才接:

  • 磁盘上已经有 → 接,零网络
  • 磁盘上没有、匿名就能拿到 → 放手,交回 WebView 自己拉(它那套比我们快)
  • 非带令牌不可的(私有仓库的图,WebView 手上没令牌)→ 才由我们来,而且是学着来的:
    第一次撞 404 补令牌成功后记下这个仓库,以后首趟就带,连那趟 401 都省了

预热也学会了让路:延后 1.2 秒再动(等屏幕上的图先拉完)、从 3 条并发改成 1 条、
并且不收屏幕上的图(它们此刻正由 WebView 拉,再下一遍是同一份字节买两次单)。

同一份附件还补了「双路竞速」:主路超过 2 秒没出图就把备用路点着,谁先回来用谁 ——
不用再等主路走到超时(十几秒)才换。主路先到的话第二趟压根不发起,快图不付两份流量。

同一版里还有:点议题里的图片,看的是大图;长按,才是打开那个页面

议题里点一张截图,弹出来的不是大图,是一个顶栏写着 GitHub、下面一片白的窗口
(github.com/user-attachments/assets/…)。两个原因叠在一起:

  1. 官方渲染的正文把上传的截图包在 <a target="_blank"> 里,而 App 的外链委托挂在
    document 的捕获阶段,跑在图片自己的 onclick 之前 —— 一点图片,先被外链
    委托接走、把内嵌浏览器打开,「查看大图」永远轮不到出场;
  2. 链接规整还把附件地址站内化成了 #/user-attachments/assets/…,可它不是站内
    页面,Router 不认识,最后照样掉进那个内嵌窗口。

现在:点图片 = 查看大图;长按图片(约半秒)= 打开它指向的那个页面;
点图片边缘(链接本体)仍是打开页面 —— 行为没丢,只是不再抢「看图」的默认位。
附件地址也从站内化规则里摘了出来,它本来就该是外链。

同一版里还有:收藏夹能分类了 —— 隔层就长在收藏夹里

官方客户端的「已添加星标」里有一层「我的列表」(Lists),把 Star 过的仓库
归进不同分类。这个功能 GitHub 至今没有开放 API(REST 里翻得到 /user/starred,
翻不到任何 list 端点),所以这版照着官方的交互做成了本机功能,并且直接
长在收藏夹里,不另立入口:

  • 切到「收藏夹」→ 顶部多一排横滑分类:全部收藏 / 你建的各个列表 / +新建;
    点哪个看哪个,长按某个分类可以重命名、删除(删除要二次确认);
  • 长按仓库条目(或仓库页「更多」菜单里的「加入列表」)弹出勾选层:
    勾上 = 归进这个分类,再点 = 移出,勾完弹层不关,可以连着归几个;
  • 归进分类的仓库自动也算收藏 —— 单击「收藏」按钮的老习惯一点没变;
    从分类里移出不影响收藏,从收藏夹移出也不影响分类;
  • 分类里存的是仓库快照(名字 / 描述 / 星数 / 语言),点开先画快照再悄悄刷新,
    断网也不会白屏。

数据保存在这台设备上(按登录账号隔离),不占 GitHub 的任何请求额度;
换设备不跟着走 —— 这是「官方没开 API」之下唯一不偷密码、不走第三方的做法。

同一版里还有:已读通知,这次是真的能删干净了

「通知 → 已读」里点删除,通知当时没了,一刷新又全部回来 ——
这个幽灵现象查到最后,根子比想象深:

删除动作其实一直是成功的(GitHub 的「标记 Done」返回 204,没毛病)。
真正的坑是 GitHub REST API 不过滤 Done 状态:标了 Done 的通知,
拉通知列表(all=true)时照样原样返回,而且返回数据里没有任何
字段能认出它已被 Done(官方最新的 API 版本也没加)。网页版收件箱
用的是带 is:done 过滤的内部接口,第三方用不了 —— 所以「已读」
列表一刷新,刚删的全回来了。

REST 层面无解,这版用本机记账补上:

  • 删除成功的通知记进本机「已清除清单」(按账号隔离,最多 500 条),
    渲染列表时自动挡掉 —— 删除、刷新、重启 App,都不会再回来;
  • Done 之后那个议题/PR 有了新动静,GitHub 会把它复活成未读送回
    收件箱 —— 未读的一律放行,新消息一条不漏;
  • 菜单新增**「清空已读」**:一键把所有已读通知标记为 Done 并记账,
    未读通知一根汗毛都不动;
  • 多选删除只移除真正成功的条目,真失败的留在列表里告诉你;
  • 顺带治好一个哑巴病:「点开通知后还是未读」—— 单条标已读之前
    用错了请求方法(PUT),改回文档的 PATCH 后真正生效。

同一版里还有:发布列表支持长按删除

仓库页「发布」标签里,以前只能新建发布,想删掉一个发错的版本、
或者给已经发出的版本补传漏掉的 APK,都得去网页版操作。现在
长按任意一条发布弹出菜单:

  • 上传 APK 附件 —— 从手机里选一个文件直传到这条发布,
    走和创建发布同一条原生二进制通道(单个不超过 200MB,
    APK / AAB / ZIP 等任意文件都行)。如果这条发布里已有同名附件,
    会先删旧的再传新的,不会出现两份同名文件;传完自动刷新列表;
  • 删除此发布 —— 危险确认(红色按钮、写明会连带删掉几个附件)
    后调用官方接口删除;删完这条就从列表里消失,全删光自动回到空态;
    git 标签会保留(GitHub 的规矩:删 Release 不删 tag);
  • 查看发布详情 —— 和点条目进去是同一个页面;
  • 没有仓库写权限时,长按只提示「需要仓库写权限」,不出菜单 ——
    看得见管不着的按钮只会招人烦。

手势和通知、收藏夹是同一套:点一下照常进详情,长按 550ms 才出菜单
(比标准 500 稍长,快滚列表不容易误弹;长按松手带出的那次误触点击
会被吞掉,不会串进详情页)。

同一版里还有:两步验证器(TOTP)—— 手机上直接出 6 位动态码

登录 GitHub、Google 这类开了两步验证的网站,除了密码还要填一个
每 30 秒变一次的 6 位动态码。以前得切到另一个验证器 App 去看,
现在 githup 自己就能算:

入口 —— 「我的 → 两步验证器」,不占底部标签页。密钥只存在你手机上,
算法也在本机跑,不联网、不需要登录 GitHub,飞机上照样出码。

添加账户的三种方式:

  • 扫一扫 —— 对准网站上的二维码,自动识别、自动填入,
    实时码立刻开始跳动;
  • 从图片识别二维码 —— 先把二维码截图存下来,再从相册选图;
  • 从剪贴板粘贴 —— 网站二维码下面通常有一行「密钥」或
    「手动输入」的字母数字,复制过来粘进去就行。

二维码识别是自己写的纯 JS 解码器,没有引第三方库 —— 定位图案、
透视校正、RS 纠错都是本仓代码,整包只多了几十 KB。

后台也能看 —— App 退到后台时,通知栏会自动常驻一个实时刷新的动态码,
回到 App 里自动收起;一个账户都没添加时不占通知栏。这件事不需要去设置里
开开关,本来就是跟着账户走的。

顺带修掉两个真机上才会显形的问题:
① 扫码按钮根本不显示 —— 选文件的方法挂在 window.Native 上,
代码里却写成 window.API,于是可用性自检恒为假,按钮被直接隐藏;
② 点保存没反应 —— 保存按钮在弹层的 .sheet-foot 里,而绑定代码
只在 .sheet-body 里找它,查到 null 后抛异常中断,处理器从未挂上。

密钥只存在这台设备上,卸载或清除数据后就没了。重要账户建议
同时在网站保存一份恢复码,免得手机丢了进不去。

版本号只动第三位(1.2.13 → 1.2.14),按 README 的规矩属于可选更新。
请更新这一版:保持 1.2.13 的话,App 会认为你已是最新版本,
拿不到这个修复。


githup v1.2.13(免费翻译引擎扩容)

Choose a tag to compare

@Buwrt Buwrt released this 26 Sep 17:39

githup v1.2.13(免费翻译引擎扩容)

这一版只动翻译引擎,没有界面改动。

免费引擎从 2 家扩到 5 家

以前真正在跑的免费引擎只有有道匿名接口和 MyMemory(有日限额,垫底兜底)。
可选项其实早就写好了 —— DeepL、Google、设备端(离线)—— 但被一句
「海外 / 需探测的引擎不进自动池」挡在外面。

当初挡得有道理:国内连 DeepL / Google 大概率不可达,设备端翻译在 WebView 上
直接不响应。一组文本摊上它们,整页就得陪着等满 15 秒的连接超时。

这次放回来,代价用三重措施兜住:

措施 作用
3 秒专属闸门 这三个引擎单独一道 3 秒超时,不是全局的 15 秒。3 秒没结果就判失败,立刻交给下一家接力
权重垫后 有道 5 / DeepL 4 / Google 3 / 设备端 2 / MyMemory 1。页面组数少时它们根本轮不到,组数多时只占小头
连续失败摘牌 真派到又没翻出来,走 poolFail,连续几次就把它摘出转轮,损害只落在头几组

付费优先

有道开放平台(权重 8)、小牛翻译(7)、百度(6)排在免费引擎之前。
不是靠 if 判断,是靠各家自己的 ready():没填密钥就不进池,
填了就自动当主力。所以免费用户看到的顺序是:

有道 → DeepL → Google → 设备端(离线)→ MyMemory(兜底)

顺序实际由 share 权重决定(转轮按权重降序展开),不是只看 ORDER 数组。

同一版里的另一项:首屏竞速

最靠近视口的那组文本同时交给两家,谁先回来谁占坑,慢到那家的译文照样写进
共享缓存给后面几组用。改善的是长尾(「有时候特别慢」的那一半),不是中位数。

版本号只动第三位(1.2.12 → 1.2.13),按 README 的规矩属于可选更新。

githup v1.2.12(返回上一层后,上一个页面不再赖在屏幕上)

Choose a tag to compare

@Buwrt Buwrt released this 26 Sep 15:46

githup v1.2.12(返回上一层后,上一个页面不再赖在屏幕上)

这一版没有新功能,只修一个 bug:从搜索页点进一个仓库(或任何详情页),
加载还没完成就按返回 —— 顶栏已经变回「搜索」,屏幕中间却还留着上一个页面的内容。
两个页面缝在一起。

版本号只动第三位(1.2.11 → 1.2.12),按 README 的规矩属于可选更新:
装了 v1.2.11 的会收到提示,不更也能继续用。


一、这个画面是怎么来的

不是返回键的问题。顶栏正确、内容没换,这个形状本身就说明路由已经退回去了 ——
是内容被人从底下顶掉的。

每个页面的 render 都是同一套节奏:

先同步铺一层骨架  →  发请求  →  响应回来把整块内容写进 #view

问题出在最后一步。真实时序:

时刻 发生的事
t0 搜索页点进仓库 → 画出仓库骨架,发出 /repos/tisfeng/Easydict
t1 你按了返回 → 路由退回搜索页,顶栏变回「搜索」,#view 换成搜索页
t2 仓库的响应这时才落地 → 回调照旧 host.innerHTML = ...
t3 刚画好的搜索页被整个覆盖 → 缝合画面

回调不知道页面已经换了人。所以这 bug 只在网络比手指慢的时候出现 ——
网速好的时候响应早就回来了,你永远撞不上它。

顺带一提:搜索页自己早就有 renderSeq 防这个,所以它不会自己盖自己;
但那是补丁不是制度,其余页面全是裸奔的。

二、改法:给 Router 加一把「渲染纪元」令牌

Router.render() 每次换一把新令牌(viewEpoch),页面在 render 里把当前纪元
拍下来,异步回调落地前先对一下纪元 —— 对不上就整份丢弃。

// app.js · Router.render()
Router.viewEpoch = (Router.viewEpoch || 0) + 1;

// page-repo.js · P.repo.render()
var epoch = window.Router.viewEpoch;
return window.API.get('/repos/' + full, ...).then(function (r) {
  if (epoch !== window.Router.viewEpoch) return;   // 页面已换人,整份丢弃
  paint(r.data, true);
}).catch(function (e) {
  if (epoch !== window.Router.viewEpoch) return;   // 错误框一样会顶掉新页面
  host.innerHTML = UI.errorBox(e);
});

两个容易漏的点,都护住了:

  • 错误框也要拦。请求失败时那个「请检查网络后下拉重试」的框同样是整块写进
    #view,一样会把新页面顶掉。
  • 改顶栏标题的回调也要拦。议题页、用户页的回调里会调 App.title(),
    不拦的话内容没被覆盖、顶栏却被改回去了 —— 那是另一种缝合。

写子容器(#tabbody、#nlist 这类)的回调不需要这层保护:页面被清空后子容器
已经脱离文档,写了也看不见。

三、接入的页面

页面 文件
首页动态 page-home.js
仓库详情 page-repo.js
议题 / 提交 / Release / 运行详情 page-detail.js
用户主页 / Gist / 我的 page-user.js

另外顺手修了一处:Gist 页渲染时用了 CSS.escape,个别老 WebView 上没有这个 API,
缺了它整个 Gist 页会挂成错误框。加了字面量兜底。

四、验证

新建 jsdom 集成测试 41 项,用桩桥扣住响应来模拟「网络比返回键慢」:

结果
修复后 41 / 41 全绿
反证(把 9 处防护全删掉,跑同一份测试) 24 / 41,红 17 项

反证里 A1 精确复现了用户截图的画面 —— 迟到的仓库响应把搜索页顶掉,#view 里
冒出 repo-head。这条红了,才说明测试真的盯着这个 bug。

既有 14 个回归脚本无回退。


附:改动清单

文件 变化
app/src/main/assets/web/js/app.js Router:新增 viewEpoch 递增 + 说明竞态的注释
app/src/main/assets/web/js/page-home.js 首页动态接入
app/src/main/assets/web/js/page-repo.js 仓库详情接入
app/src/main/assets/web/js/page-detail.js 议题 / 提交 / Release / 运行详情接入
app/src/main/assets/web/js/page-user.js 用户主页 / Gist / 我的接入,另修 CSS.escape

Java / Kotlin 未改业务逻辑(GuardKeys.java 由防护链脚本按新版本号重新生成)。

githup v1.2.11(液态玻璃:背景真的会弯)

Choose a tag to compare

@Buwrt Buwrt released this 26 Sep 12:28

githup v1.2.11(液态玻璃:背景真的会弯)

这一版没有任何新功能,只换了一件事:玻璃的材质。

以前的顶栏、底栏、弹层、卡片是毛玻璃——背景先糊掉(blur 20~30px)再透上来。
柔和是柔和,但有个硬伤:背景内容本身不弯折。水下的东西平着过去,再怎么看都不是水。

液态玻璃(liquid-glass)走另一条路:用内联 SVG 位移滤镜把背景扭过来,
玻璃给用户看的不只是「透」,而是「折射」。

版本号只动第三位(1.2.10 → 1.2.11),按 README 的规矩属于可选更新:
装了 v1.2.10 的会收到提示,不更也能继续用。


一、为什么必须整套换

一句话的物理约束:

模糊是折射的天敌。blur 超过 6px,形变就糊得看不见了。

原来的顶栏是 blur(20px)、底栏 blur(26px)。直接把位移滤镜叠上去不会有任何视觉效果,
水面当场死掉。所以这不是「加个效果」,是整套材质取值倒过来:

以前(毛玻璃) 现在(液态)
模糊量 20~30px 3px(必须 ≤ 4px)
面罩透明度 .62 .30
噪点 .22(毛玻璃的灵魂) .06(液态的敌人)

噪点从 .22 压到 .06 是关键:水面看着是水,全靠背后那块扭曲的图案看得清,
铺一层颗粒上去等于给它蒙雾——GPU 每帧照旧算位移,肉眼什么都看不见。
留 .06 只是防止大面积渐变出现色带。

二、一个差点让整件事白做的前提

原来的背景是全不透明的。

.card { background: var(--bg); }        /* #ffffff 实色 */
.list { background: var(--bg); }
/* 画布 --canvas 也是实色 */

实色透实色——不管挂什么滤镜,输入都是同一块颜色,把纯色扭多少像素它还是纯色。

所以先给画布铺了一层能被折射的内容:

  • 三团很淡的径向色晕——提供大块明暗变化
  • 一层极淡的斜纹——这是关键。折射看不看得出来取决于背后有没有高频图案,
    光是平滑渐变,扭了也看不出来
  • background-attachment: fixed——让背景不跟着滚,卡片从它上面滚过去时图案相对移动,
    才有「内容从水面下流过」的观感

深色模式配了另一组更冷的色晕。

三、覆盖范围,以及两处刻意的例外

顶栏、底栏胶囊、弹层、提示条、列表卡片统一到同一套语言。

其中两处故意不一样:

元素 为什么降力度
弹层(.sheet) 它是要静下来读的东西,底下全是扭动会累。折射力度减半、面罩加厚,再补一层 brightness(1.18) 抵消遮罩的压暗
列表卡片 惯性滚动时每帧都要为每张卡片重算位移贴图,若干张同屏就是「每帧 × 张数」——这是实打实的掉帧,不是理论顾虑

另外两处小改:

  • 面罩变薄、噪点去掉之后,撑住玻璃形状的就只剩边缘那两道 inset(上缘受光、下缘背光),这次把它加重了。
  • 按压动效换成弹簧曲线 .34,1.56,.64,1(末段 >1 会冲过头再收回)。列表行不加:
    它在 overflow:hidden 的卡片里,缩放会被裁出白边,一行缩放还会顶着相邻行,整列看着在抖。

四、踩到的两个 CSS 陷阱

陷阱一:含 url() 的折射行不能包进 var()。

把 saturate(...) blur(...) 抽成变量省几行——不行。自定义属性一旦替换出的值无效,
该声明就是 invalid at computed-value time,属性会掉回 inherit/initial,
不会回落到同属性的前一条声明。老 WebView 会直接得到一个「没有 backdrop-filter」的顶栏,
而不是兜底。字面写的话,不认的引擎当作解析失败整条丢掉,兜底自然留下。

于是每个元素写成四行:

backdrop-filter: saturate(...) blur(20px);            /* 兜底 */
-webkit-backdrop-filter: saturate(...) blur(20px);    /* 兜底(老 WebView 只认前缀) */
backdrop-filter: url(#lens) saturate(...) blur(3px);  /* 真折射 */
-webkit-backdrop-filter: url(#lens) saturate(...) blur(3px);

陷阱二:<defs> 不能用 display:none 藏。 被隐藏的元素不进渲染树,
引用它的滤镜会整条失效,玻璃会悄悄退化成「一层几乎不糊的半透明面」——
所以改用 0 尺寸 + 绝对定位躲起来。

五、性能(这句是真话)

液态玻璃是三档玻璃里最贵的一档:backdrop-filter 要逐帧重算它覆盖的那块背景,
位移滤镜更贵。已经做了两处降级(列表走 soft 档、朴素样式的隔离完好),
但中低端机型上滚动变卡是有可能的。

真觉得卡:设置里关掉「iOS 风格」,立刻回到从前的实体栏 + 纯色背景。
这个开关这次一行没动,是改动的第一约束。


附:改动清单

文件 变化
app/src/main/assets/web/index.html +26 行:内联两片位移滤镜 #lens / #lens-soft
app/src/main/assets/web/css/app.css +173 行:材质取值、背景层、折射回落、弹簧曲线

Java / Kotlin 一行未动。 全部改动隔离在 html[data-nav="glass"] 这条轴上,
朴素样式完全不受影响(有脚本逐条校验)。

githup v1.2.10(Actions 运行记录:按工作流筛选 · 跑废的可以删)

Choose a tag to compare

@Buwrt Buwrt released this 26 Sep 08:25

这一版加的是 Actions 页面上一直缺的两件事:按工作流筛选,以及删除运行记录。
功能来自社区 PR #9(mymine 提的),合进来之前补了三处,下面单独说。

版本号只动第三位(1.2.9 → 1.2.10),按 README 的规矩属于可选更新:
装了 v1.2.9 的会收到提示,不更也能继续用。


一、按工作流筛选:一个仓库六个工作流时不用再翻页

以前 Actions 页只有一排状态分段控件(全部 / 成功 / 失败 / 运行中)。仓库里挂着
CI、发布、依赖更新好几个工作流时,想看其中某一个的构建记录,只能在一整页里翻。

现在状态分段下面多了一个「工作流:全部」按钮:

  • 点开列出该仓库的全部工作流(停用的会标出来);
  • 选中后只显示这个工作流的运行记录;
  • 两个筛选同时生效,并且都写进地址 —— ?status=failure&wf=123。

写进地址这件事是有用的:按返回、或者把链接分享给同事,都能回到同一个筛选组合,
而不是「我刚才筛的是哪个来着」。

一个容易踩空的点:/actions/runs 不支持按工作流过滤。得换到
/actions/workflows/{id}/runs 才拿得到数据。这版走的就是后面这条端点 ——
PR 作者这一处判断是对的。

二、删除运行记录:跑废的那条不用留着

网页端早就有的「Delete workflow run」,App 里一直没有。现在两处都能删:

入口 位置
列表 每条运行记录行尾的 ⋮(只有对该仓库有写权限时才显示)→ 删除运行记录 / 在浏览器打开
详情 单条运行详情页右上角菜单 → 删除运行记录(分隔线下面那一条)

删之前有二次确认,按钮是红色的「删除」,并且写明「日志与产物一起删、删了没法恢复」。
删除成功后列表自动刷新。

报错翻成人话,不再对着一句 Forbidden 发呆:

状态码 提示
403 没有权限(需要对该仓库的写权限)
404 运行记录不存在或已被删除
409 该运行正在进行中,先取消再删除

顺带说一个实现上的坑(不写下来的话下一个人还会踩):全局的 data-go 委托跑在
捕获阶段,在冒泡里 stopPropagation() 是拦不住它的 —— 不特殊处理,点行尾的 ⋮
就会顺带跳进详情页。所以这一行按 App 的约定自己绑 onclick 并置 __bound,
让全局委托跳过,由它自己区分「点 ⋮ 开菜单 / 点行进详情」。

三、合并 PR #9 时补的三处

PR 本体的方向没问题,代码质量比一般的 PR 高(上面说的两个坑它都提前判断对了)。
合进来之前补了三处,都属于「功能在,但边界没兜住」:

1. ?status= 收回白名单

PR 把原来「三选一」的收敛改成了原样透传。链接里混进别的值(手敲的、旧版本分享
出去的、别的客户端生成的)会一路直通 GitHub,对方回 422,整页变成一个报错框。
现在认不出的值一律退回「全部」。

2. ?wf= 一并收敛

它会被拼进接口的路径里(/actions/workflows/{wf}/runs),等于让用户控制一段
URL 路径。现在只放行数字 id 和 xxx.yml 文件名,其余退回「全部」。

3. 删除的 promise 链 return 出去

原来 deleteRun() 没有 return,调用方(详情页、行尾菜单)拿不到结果,不知道什么
时候该刷新。现在成功 resolve true、失败 resolve false(失败已经提示过,不再
往外抛)。详情页再补一层「仓库页还没加载好」的判断 —— 不判断的话那种情况下点了
就是毫无反应,看着像卡死。

另外,PR 里夹带了一个误提交的文件:把这次改动的 git diff 整个存成了
githup-actions-filter-delete(206 行),没有合进来。

四、回归

新增 58 项针对性测试,覆盖筛选拼接、取数端点、⋮ 的分流、删除的确认与失败提示。

反证过(改一行代码就说修好了,是没法证明的):

被测的代码 结果
修复后 58 / 58
退回 PR 原样 53 / 58 —— 红的正是上面那三处
只补第一处的「半吊子」 55 / 58

既有回归全绿:tab 栏与返回 47 + 36、仓库页头部 22、Markdown 26 + 29、站内链接 48、
底栏贴底 26、安全区 46。第 3 环(前端资源完整性,21 个文件)21/21 通过。

五、下载与校验

项 值
文件 githup-1.2.10.apk
大小 926,286 字节(约 905 KB)
SHA-256 391ee29c05eac58ebdd9af7d0669e1cbf4b03b16680b9707c5ed6030f25c49cc
versionName / versionCode 1.2.10 / 1002010
最低系统 Android 7.0(API 24)

包内自带两道自校验:签名自校验(证书指纹不符拒绝运行)+ 源码指纹
(对源码文件算整体 SHA-256 内嵌,被改过会报出来)。本次源码指纹 SRC_SHA256:

c017b60205f4a0e61a12e03cfad99f5a68c27b09604b8365809d02659273f8cc

签名证书与之前一致,可直接覆盖安装,不用卸载。

githup v1.2.9(仓库页按返回一律先回代码 · 朴素样式底栏贴回屏幕底边)

Choose a tag to compare

@Buwrt Buwrt released this 26 Sep 03:57

这一版修的是一个真实现象:在仓库页的议题 / 拉取请求 / Actions / 发布 / 更多,
以及「更多」里进去的提交记录、分支、标签……上按返回,会去哪儿全看你是怎么进来的。

一、以前:返回键把走过的 tab 倒着走一遍

从「代码」连点「议题 → 拉取请求 → 发布」,想退出去得按三次返回:
先退到「拉取请求」,再退到「议题」,最后才回到「代码」。用户想要的其实是
「退出这一个 tab」,不是把刚才走过的路倒着回放一遍。

更难受的是另一条路:从搜索结果或者通知里直接点进某个仓库的「议题」——
历史里压根没有「代码」这一条,一按返回整个仓库页就没了。明明人还在仓库里,
却被弹回了搜索页。

根因就一句话:返回键走的是 history 逐级回退,而 tab 之间每次切换都往历史里堆一条。

来路 以前按返回
代码 → 议题 → 拉取请求 → 发布 退到「拉取请求」→「议题」→ 三次才回到代码
搜索结果 → 议题 历史里没有「代码」,直接退出仓库页
通知 → 发布 同上

二、顺带修掉:回到代码了,那一排 tab 却还停在右边

修完上面那条,现象变成「页面确实回到代码了,可那一排还停在后面」——
高亮是「代码」,可它在视野外,看着像没生效。

根因在位置记忆:返回走的是 replace,不是点击,于是渲染时走「按记忆原样恢复」
那条路,而记忆还停在右边(你上次为了够到「发布」拉过去的位置)。

现在 高亮落在「代码」时无条件把这一排拉回最左,并把记忆一起清掉 ——
不然下次回来又被拽到右边。「代码」是这一排的第一个,停在右边没有任何意义。

只认代码 tab:在「发布」「议题」上刷新,位置照旧不动(那是「刷新后位置不跑」,
另一条需求,不能一起改没)。

三、现在:一次返回到代码,再一次返回出仓库

两处一起改:

  • handleBack() 新增一步 —— 只要人在这个仓库、且不在代码 tab,返回就落到代码 tab;
    已经在代码 tab 了,才继续往上退(回搜索、回首页……)。这里用 replace 而不是 go:
    再 push 一条的话,历史会攒成「代码 → 议题 → 代码 → 议题」,按返回变成在这两页之间来回跳,
    永远退不出去。
  • pushRoute() 把「同一个仓库里换 tab」认定为横向切换,跟底部五个标签一样不堆历史。
    那一排 tab 是仓库页内部的并列视图,来回切不该留下痕迹。

于是历史栈里始终只有「进仓库前那一页 + 当前 tab」两条:
一次返回到代码,再一次返回出仓库。

四、该逐级退的地方,一步都不能跳

新规则只管 tab 层,下面是刻意不动的:

位置 按返回
tree / blob 文件浏览 它们本来就在代码 tab 里,照旧逐级退回仓库根
议题详情 / PR 详情 / 运行详情 / 提交详情 / Release 详情 先退回它自己那个 tab
搜索 / 通知 / 个人页 / 下载管理 与仓库无关,不受影响

判据是「这个路径是不是某个仓库的 tab 层」—— /owner/repo 后面最多只跟一个 tab 名,
带文件路径、议题号、commit sha、tag 的都不算。

五、同版追加:朴素样式下,底栏重新贴回屏幕底边

这一版发出之后又报来一个现象:关掉「iOS 风格」回到朴素那条实心底栏,
在有的机器上这一排没有贴着屏幕底边,和底之间留着一段空隙。

玻璃那一套从来没有这个问题。(版本号不变,直接重打包。)

为什么只有朴素样式看得出来。 底栏那一条是:

#tabbar { bottom: calc(var(--nav-lift) + var(--safe-b)); }

玻璃下 --nav-lift 是 14px —— 它是浮在半空的胶囊,任何误差都混在这 14px 的悬浮
感里,眼睛认不出来。朴素下它是 0:底栏到屏幕底边的距离就等于 --safe-b 这一个
数,原生量错多少,这里就空多少。同一个 bug,一半的人看不见。

而那个数是这么错的。

第一层是重复让位。MainActivity 只声明了 LAYOUT_FULLSCREEN,没有声明
LAYOUT_HIDE_NAVIGATION —— 也就是说系统已经替我们把导航栏那条留白留出来了,
WebView 本身的矩形根本就不含它。这时候前端再照着「导航栏有多高」让一次,
等于凭空多出一块。

第二层在 minSdk 上。它是 24,于是 Android 7~10 一律走这条分支 —— 直接读资源:

int idBot = r.getIdentifier("navigation_bar_height", "dimen", "android");
if (idBot > 0) bottom = r.getDimensionPixelSize(idBot);   // 恒为 48dp

那是 ROM 写死的上限值,跟「这台机器当下画没画导航栏」毫无关系:

机器 屏幕底部实际情况 资源里写的
Android 10 手势导航 只有一条细线 48dp
Android 9 三键导航 有一条 48dp(这条碰巧对了)
实体 Home / mBack 的老机型 根本没有软导航栏 48dp

同一份注释里还写着「手势导航下导航栏高度是很小的,不能写死 48dp」——
可那条约束只落实在 SDK ≥ 30 那一支,else 里照旧写死。「有的手机上」就是它们。

改法三处:

  1. 原生侧从「查配置」改成「量」。 拿 WebView 自己在屏幕坐标里的下沿去比屏幕
    真实底边(getLocationOnScreen vs getRealMetrics):

    • 够不到 → 系统已经留白了,页面自己不用再让,返回 0;
    • 够得到 → 页面确实铺到了屏幕底,这时只看系统条这会儿画没画出来,
      没画返回 0,画了才返回它真实高度;
    • 布局还没走完时不猜,同样返回 0 —— 宁可贴底,也不要让底栏飘在半空中。

    这套判据不依赖任何「照厂商配置估算」的量,ROM 怎么改都一样成立。

  2. CSS 把「底栏离屏幕底边」抽成 --nav-bottom 单独一条。 玻璃照旧用
    --safe-b,朴素钉死为 0 —— 朴素那一格的语义本来就是「贴着屏幕底边」,
    从此不再受原生那个数的影响。

  3. 前端加一道防御上限。 异常 ROM 报出几百 px 时截断到 min(32px, 屏高的 8%)。

顺带把 .fab、#toast-root、.sel-bar、#view 的 padding-bottom 一起改了 ——
它们都按「底栏上沿」定位,必须跟着同一个量走,否则就变成「底栏贴住了底、
内容却多空出一截」。

会不会被系统条压住?只有在「页面真的铺到屏幕最底 + 三键导航」时才可能,而这时
系统条是浮在页面之上的一层(导航栏已被设为透明),底栏主体仍然是完整的。
相比之下「离底一大截」是实实在在的错位,两者不在一个量级上。

代码自校验(不需要服务器)

包内自带两道:签名自校验(证书指纹不符直接拒绝运行)+ 源码指纹
(对源码文件算整体 SHA-256 并内嵌,被改过就报出来)。本次源码指纹(SRC_SHA256):

270b231eb90d63da48be74fa51c7f2f260d36a5a4d85fb441888a6d9e0d420fa

安装信息

项 值
文件名 githup-1.2.9.apk
大小 923,690 字节(约 902 KB)
SHA-256 69ba30e1cb5bad00a2fba9a834d7fc9cb3b6671dc63d3457b2702a88cdda456d
versionName / versionCode 1.2.9 / 1002009
应用名 / 包名 githup / com.hubmobile.app
最低系统 Android 7.0(API 24)

更新时才提醒,已是最新版完全静默。签名证书与之前一致,可直接覆盖安装,不用卸载。

更新时才提醒,已是最新版完全静默。签名证书与之前一致,可直接覆盖安装,不用卸载。