chore: drop CNB config; Android package is com.htmake.readerpro

- Remove .cnb.yml and CNB references in the Dockerfile and Compose file; the
  Compose default image is hectorqin/reader-pro:latest.
- Rename the Android applicationId/namespace and Kotlin package from
  cool.cnb.reader to com.htmake.readerpro (shortcuts, ProGuard, shell check).
  Installs under the old package are a different app and are not migrated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hectorqinandClaude Opus 5.5 committed 2026-10-30 02:03:45 +00:00
1 parent 32595c87a5
commit 415a9f5dd3
35 files changed
+74 -326

No files matched your search

-250
View File
@@ -1,250 +0,0 @@
# CI:服务端(Node/TypeScript)、客户端(Web/TypeScript)、Android 壳(Kotlin)。
#
# 每个任务都用 ifModify 限定到自己那部分,避免改一行 README 就把三条流水线
# 全跑一遍。
# 服务端与客户端都是 Node 生态,共用同一个镜像锚点。
.node-env: &node-env
docker:
image: node:24
.server-check: &server-check
- name: install
script: cd server && npm ci
- name: typecheck
script: cd server && npm run typecheck
- name: test
script: cd server && npm test
- name: build
script: cd server && npm run build
.client-check: &client-check
- name: install
script: cd web && npm ci
- name: typecheck
script: cd web && npm run typecheck
- name: test
script: cd web && npm test
- name: build
script: cd web && npm run build
# UI 评审:把真实产物在真实 Chromium(390×844)里逐屏渲染、截图并测量。
#
# 和 `client-check` 分开,因为两者回答的问题不同:那里问「功能对不对」,
# 这里问「看起来对不对」—— 面板是不是盖住了整页、底部页数和可翻次数是不是
# 一个数、状态提示出现时正文会不会被推下去。这些都能在功能全绿的同时是错的,
# 而它们的形态只有截图和量出来的数字能发现(见 docs/ui.md §8)。
#
# 截图作为制品留存,所以一次失败的评审可以被人看到,而不是只看到一行报错。
.ui-review: &ui-review
docker:
# 镜像里带 Chromium。评审工具只用 CDP 协议驱动浏览器(见
# `web/tools/ui-review/cdp.mjs`),所以这里只要一个能跑的 Chromium,
# 不需要 playwright 的运行时依赖。
#
# `/usr/local/bin/chromium` 是镜像自己提供的路径之一,工具会按
# CHROME_PATH → chromium → chromium-browser → google-chrome 的顺序找,
# 找不到就直接报错,不会静默跳过整轮评审。
image: mcr.microsoft.com/playwright:v1.49.1-noble
stages:
- name: install
script: cd web && npm ci
# 生产产物,不是 dev server:被评审的必须是用户真正拿到的那份。
- name: build
script: cd web && npm run build
- name: ui-review
script: cd web && npm run ui:review:only
- name: archive-screenshots
script: |
echo "UI 评审产物:artifacts/ui-review/react-app/"
ls -la artifacts/ui-review/react-app/
main:
push:
- name: server-ci
<<: *node-env
ifModify:
- server/**
stages:
- *server-check
failStages:
- name: report-failure
script: echo "服务端 CI 失败:$CNB_BUILD_FAILED_MSG"
- name: client-ci
<<: *node-env
ifModify:
- web/**
stages:
- *client-check
# UI 评审只在界面相关文件变化时跑:它要装 Chromium、渲染八屏、量尺寸,
# 比 client-ci 慢一个量级,而它验证的每一条规则都只被样式与组件改动影响。
- name: client-ui-review
<<: *ui-review
ifModify:
- web/src/ui/**
- web/src/styles/**
- docs/ui.md
- web/tools/ui-review/**
- web/index.html
failStages:
- name: report-failure
script: echo "客户端 CI 失败:$CNB_BUILD_FAILED_MSG"
# Android 壳。
#
# 先跑一个不依赖 JDK 的静态检查(秒级),再编译。原因是桥接契约的两半分别
# 用 Kotlin 和 TypeScript 写,互相没法类型检查:客户端调用一个壳里没有的
# 方法是最容易出、也最难在本地发现的错误。静态检查把它提前到这里。
#
# 触发条件包含 `web/src/formats/types.ts`:壳里分支的渲染模式词表来自它,
# 改一个词就会让整条原生渲染通道静默失效,而 Kotlin 编译器不会知道。
# 镜像走 Docker Hub 命名空间,CNB 会自动经 Docker Hub 代理注入只读令牌。
#
# 这里曾写 `androidsdk/android-35:latest`,而 Docker Hub 的 `androidsdk`
# 命名空间**没有**这个仓库 —— 它只发布到 `android-31`(`android-33` 及以上
# 都是 404)。仓库不存在时 Docker Hub 返回 `pull access denied / repository
# does not exist or may require 'docker login'`,daemon 会重试一次,然后
# prepare 阶段直接失败:`docker pull failed: 任务执行失败,返回码:1`。整条
# 流水线还没进入任何 stage 就红了,所以 `shell-contract`/`assemble` 全是
# skipped —— 失败信息指向 `docker pull`,但真正的问题是镜像名指向了一个
# 不存在的仓库,与令牌、网络、代理都无关。
#
# 改用 `mobiledevops/android-sdk-image`:Docker Hub 上真实存在且在维护的
# 社区镜像(2026-07 仍有更新,多架构 amd64/arm64)。它的 `packages.txt` 含
# `platforms;android-35` 与 `build-tools;35.0.1`,正好匹配本项目的
# compileSdk/targetSdk=35;基础镜像是 ubuntu:24.04 + openjdk-21(满足
# Gradle 8.11 与 AGP 8.7 对 JDK 17+ 的要求),并已预接受 SDK 许可,
# assemble 不需要再联网下载平台组件。
#
# 用 `latest` 而不是 `36.1.0` 之类的构建工具版本号:这个仓库的 tag 是按
# build-tools 版本命名的,`latest` 始终指向包含 android-35 的最新一份,
# 写死版本反而会在下一次平台升级后被遗漏。若要完全可复现可改钉具体 tag。
- name: android-ci
docker:
image: mobiledevops/android-sdk-image:latest
ifModify:
- android/**
- web/src/android-bridge.d.ts
- web/src/formats/types.ts
- web/src/ui/native-page.ts
stages:
- name: shell-contract
script: sh android/scripts/check-shell.sh
- name: assemble
script: cd android && ./gradlew --no-daemon :app:assembleDebug
failStages:
- name: report-failure
script: echo "Android 构建失败:$CNB_BUILD_FAILED_MSG"
# 客户端产物必须能被壳打包。这条单独跑,因为 `client-ci` 只证明 H5 能构建,
# 不证明 assets 里有东西——而一个空的 assets 目录会产出一个启动即白屏的 APK。
- name: web-assets-ci
<<: *node-env
ifModify:
- web/**
# 触发条件包含脚本自身与 assets 目录布局:改这段逻辑、或改 Gradle 里
# `assets.srcDirs` 指到哪,本来不会跑任何验证,而它坏掉的形态是 APK 里
# 没有客户端、启动即白屏 —— 只有跑一遍才知道。
- android/scripts/build-web-assets.sh
- android/scripts/verify-web-assets.sh
- android/app/build.gradle.kts
- android/app/src/main/assets/**
stages:
- name: stage-for-shell
script: sh android/scripts/build-web-assets.sh
# 只证明脚本写出了 assets 还不够,还要证明「写出来的东西真的会被打进 APK」。
# 脚本只知道它写到哪,Gradle 只知道哪些目录算 assets,两者之间没有任何
# 东西把它们连起来 —— 这两条路径此前就漂移过。
- name: verify-assets-are-packaged
script: sh android/scripts/verify-web-assets.sh
# 发布镜像到制品库 `docker.cnb.cool/hectorqin/reader`。
#
# 只在 `server/`、`web/` 或构建文件变更时跑:镜像里只有这两部分。文档改一行
# 也重新发布一个镜像,会让制品库里堆满内容完全相同的标签。
#
# 为什么用 CNB 官方的 `services: docker`,而不是 `docker.build` + kaniko
# 插件任务:`docker.build` 的产物只活在 runner 的本地 dockerd 里,而平台
# 起插件任务容器走的是 `docker pull <image>`,不查本地镜像列表——
# `docker.build` 那条路要求 `image:` 写成与 `dockerfile` 同名的裸名字或
# 缓存仓库地址,靠平台去跟已构建的镜像做匹配。这条流水线因此在 `main` 上
# 连错八次,报错覆盖了 `pull access denied`(被当成 Docker Hub 官方镜像)
# 与 `invalid reference format`(名字里有大写或非法字符)两类;最后是靠把
# `image:` 指向 `dockerfile-caches` 的完整仓库地址跑通的,代价是那次匹配
# 依赖把一个**缓存 digest** 写死进流水线。
#
# 换成 dind 之后整条链路上只剩标准 Docker:
# - 不再依赖平台对 `image:` 的匹配约定,也就不需要把缓存 digest 当镜像名;
# - 没有 kaniko,就不必为了 `COPY server/...` 保住大小写而绕开工作区,
# `GIT_CLONE_PATH` 那个补丁连同它带来的 `--context-sub-path` 需求一起没了;
# - 也不需要单独 `docker login`,CNB 起 dind 服务时会自动登录制品库,
# 用的是同一次构建的临时令牌,权限天然对齐。
# 之前维护的根目录 `Dockerfile` / `kaniko-plugin.sh` 随之删除。
- name: docker-image
services:
- docker
ifModify:
- server/**
- web/**
- server/Dockerfile
- .dockerignore
# 触发条件包含自身,否则改完构建逻辑没法验证。
- .cnb.yml
stages:
- name: docker build and push
# 镜像路径按「同名制品」规则走,即 `docker.cnb.cool/hectorqin/reader`,
# 与 README、docker-compose.yml 里引用的地址一致。用
# `CNB_REPO_SLUG_LOWERCASE` 而不是写死 `hectorqin/reader`:Docker 要求
# 仓库名全小写,而 CNB 的仓库路径本身可能带大写。
#
# 构建上下文是仓库根,`server/Dockerfile` 里那句 `COPY web/...` 依赖
# 这一点(客户端产物要打进服务端镜像)。`services: docker` 下的任务
# 工作目录就是仓库根,不需要额外指定 `-f`/`--context`。
#
# 两个标签:分支名给「跟 main 走」的部署用,短 commit 给需要回滚到
# 某个确切版本的情况用。`main` 分支上两者分别是 `main` 和当次提交。
# 逐个 `docker push` 而不是 `--all-tags`:文档里的写法就是显式推标签,
# 不想把这条流水线绑在一个没被文档覆盖的 flag 上。
script: |
set -eu
image="$CNB_DOCKER_REGISTRY/$CNB_REPO_SLUG_LOWERCASE"
docker build -t "$image:$CNB_BRANCH" -t "$image:$CNB_COMMIT_SHORT" \
-f server/Dockerfile .
docker push "$image:$CNB_BRANCH"
docker push "$image:$CNB_COMMIT_SHORT"
failStages:
- name: report-failure
script: echo "镜像发布失败:$CNB_BUILD_FAILED_MSG"
"**":
pull_request:
- name: server-checks
<<: *node-env
ifModify:
- server/**
stages:
- *server-check
- name: client-checks
<<: *node-env
ifModify:
- web/**
stages:
- *client-check
# PR 阶段只跑不依赖 JDK/SDK 的那半(秒级)。桥接契约断裂与渲染模式词表漂移
# 是最容易出、最难在本地发现的错误 —— 两半分别用 Kotlin 和 TypeScript 写,
# 互相没法类型检查 —— 而它们恰好都不需要编译就能查出来。完整 assembleDebug
# 仍只在 main 的 push 上跑,PR 不该为一个 SDK 镜像等几分钟。
- name: android-contract-checks
<<: *node-env
ifModify:
- android/**
- web/src/android-bridge.d.ts
- web/src/formats/types.ts
- web/src/ui/native-page.ts
stages:
- name: shell-contract
script: sh android/scripts/check-shell.sh
+2 -2
View File
@@ -4,11 +4,11 @@ plugins {
}
android {
namespace = "cool.cnb.reader"
namespace = "com.htmake.readerpro"
compileSdk = 35
defaultConfig {
applicationId = "cool.cnb.reader"
applicationId = "com.htmake.readerpro"
minSdk = 24
targetSdk = 35
versionCode = 1
+1 -1
View File
@@ -2,7 +2,7 @@
# annotation. R8 cannot see those call sites, so without this the release build
# would strip them and the client would silently lose connectivity reporting and
# the device label.
-keepclassmembers class cool.cnb.reader.bridge.ReaderBridge {
-keepclassmembers class com.htmake.readerpro.bridge.ReaderBridge {
public *;
}
-keepattributes JavascriptInterface
@@ -1,4 +1,4 @@
package cool.cnb.reader
package com.htmake.readerpro
import android.annotation.SuppressLint
import android.content.Intent
@@ -12,11 +12,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import android.widget.FrameLayout
import cool.cnb.reader.bridge.ConnectivityMonitor
import cool.cnb.reader.bridge.ReaderBridge
import cool.cnb.reader.bridge.SpeechBridge
import cool.cnb.reader.web.NativePageView
import cool.cnb.reader.web.WebHost
import com.htmake.readerpro.bridge.ConnectivityMonitor
import com.htmake.readerpro.bridge.ReaderBridge
import com.htmake.readerpro.bridge.SpeechBridge
import com.htmake.readerpro.web.NativePageView
import com.htmake.readerpro.web.WebHost
/**
* The only activity.
@@ -1,4 +1,4 @@
package cool.cnb.reader.bridge
package com.htmake.readerpro.bridge
import android.content.Context
import android.net.ConnectivityManager
@@ -1,4 +1,4 @@
package cool.cnb.reader.bridge
package com.htmake.readerpro.bridge
import android.content.Context
import android.os.Build
@@ -7,7 +7,7 @@ import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.Toast
import cool.cnb.reader.web.NativePageView
import com.htmake.readerpro.web.NativePageView
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.Date
@@ -68,7 +68,7 @@ class ReaderBridge(
* the web layer decides when to pay that, by calling `init()` when the reader
* first opens the朗读 settings rather than on every app start.
*/
private val speechBridge = SpeechBridge(context, webView) { cool.cnb.reader.media.MediaAudioCoordinator.pauseMedia?.invoke() }
private val speechBridge = SpeechBridge(context, webView) { com.htmake.readerpro.media.MediaAudioCoordinator.pauseMedia?.invoke() }
/**
* Runs a UI-thread block and waits for its result.
@@ -1,4 +1,4 @@
package cool.cnb.reader.bridge
package com.htmake.readerpro.bridge
import android.content.Context
import android.os.Bundle
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
/** Called only on the main looper. Neither subsystem needs to know the other's implementation. */
object MediaAudioCoordinator {
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import android.content.ComponentName
import android.content.Context
@@ -18,7 +18,7 @@ import androidx.media3.session.SessionCommand
import androidx.webkit.JavaScriptReplyProxy
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import cool.cnb.reader.web.WebHost
import com.htmake.readerpro.web.WebHost
import java.net.URI
import org.json.JSONObject
import org.json.JSONArray
@@ -0,0 +1,11 @@
package com.htmake.readerpro.media
/** Launcher shortcuts select fixed local routes, never caller-provided URLs. */
object MediaLaunchRoute {
fun fragmentForAction(action: String?): String? = when (action) {
"com.htmake.readerpro.OPEN_VIDEO" -> "#/media/video"
"com.htmake.readerpro.OPEN_MUSIC" -> "#/media/music"
"com.htmake.readerpro.OPEN_AUDIOBOOK" -> "#/media/audiobook"
else -> null
}
}
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import android.app.PendingIntent
import android.content.Intent
@@ -19,7 +19,7 @@ import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import cool.cnb.reader.MainActivity
import com.htmake.readerpro.MainActivity
import org.json.JSONObject
import org.json.JSONArray
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import java.net.HttpURLConnection
import java.net.URI
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import java.net.URI
import org.json.JSONArray
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import android.content.ComponentName
import android.app.PictureInPictureParams
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import org.json.JSONObject
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import androidx.media3.common.ForwardingPlayer
import androidx.media3.common.Player
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
/** Main-thread ownership of subtitle UI callbacks; independent of player/network lifetime. */
internal class SubtitleRequestGate {
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
/** Android restricts the PiP aspect ratio to approximately 1:2.39 … 2.39:1. */
internal object VideoPipPolicy {
@@ -1,4 +1,4 @@
package cool.cnb.reader.web
package com.htmake.readerpro.web
import android.content.Context
import android.graphics.Bitmap
@@ -1,4 +1,4 @@
package cool.cnb.reader.web
package com.htmake.readerpro.web
import android.annotation.SuppressLint
import android.content.Context
@@ -13,12 +13,12 @@ import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.webkit.WebViewAssetLoader
import cool.cnb.reader.BuildConfig
import cool.cnb.reader.media.MediaLaunchRoute
import cool.cnb.reader.bridge.ReaderBridge
import cool.cnb.reader.bridge.SpeechBridge
import cool.cnb.reader.media.MediaBridge
import cool.cnb.reader.media.MediaAudioCoordinator
import com.htmake.readerpro.BuildConfig
import com.htmake.readerpro.media.MediaLaunchRoute
import com.htmake.readerpro.bridge.ReaderBridge
import com.htmake.readerpro.bridge.SpeechBridge
import com.htmake.readerpro.media.MediaBridge
import com.htmake.readerpro.media.MediaAudioCoordinator
/**
* Configures the WebView and serves the bundled client to it.
@@ -1,11 +0,0 @@
package cool.cnb.reader.media
/** Launcher shortcuts select fixed local routes, never caller-provided URLs. */
object MediaLaunchRoute {
fun fragmentForAction(action: String?): String? = when (action) {
"cool.cnb.reader.OPEN_VIDEO" -> "#/media/video"
"cool.cnb.reader.OPEN_MUSIC" -> "#/media/music"
"cool.cnb.reader.OPEN_AUDIOBOOK" -> "#/media/audiobook"
else -> null
}
}
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut android:shortcutId="media_video" android:enabled="true" android:icon="@mipmap/ic_launcher" android:shortcutShortLabel="@string/shortcut_video">
<intent android:action="cool.cnb.reader.OPEN_VIDEO" android:targetPackage="cool.cnb.reader" android:targetClass="cool.cnb.reader.MainActivity" />
<intent android:action="com.htmake.readerpro.OPEN_VIDEO" android:targetPackage="com.htmake.readerpro" android:targetClass="com.htmake.readerpro.MainActivity" />
</shortcut>
<shortcut android:shortcutId="media_music" android:enabled="true" android:icon="@mipmap/ic_launcher" android:shortcutShortLabel="@string/shortcut_music">
<intent android:action="cool.cnb.reader.OPEN_MUSIC" android:targetPackage="cool.cnb.reader" android:targetClass="cool.cnb.reader.MainActivity" />
<intent android:action="com.htmake.readerpro.OPEN_MUSIC" android:targetPackage="com.htmake.readerpro" android:targetClass="com.htmake.readerpro.MainActivity" />
</shortcut>
<shortcut android:shortcutId="media_audiobook" android:enabled="true" android:icon="@mipmap/ic_launcher" android:shortcutShortLabel="@string/shortcut_audiobook">
<intent android:action="cool.cnb.reader.OPEN_AUDIOBOOK" android:targetPackage="cool.cnb.reader" android:targetClass="cool.cnb.reader.MainActivity" />
<intent android:action="com.htmake.readerpro.OPEN_AUDIOBOOK" android:targetPackage="com.htmake.readerpro" android:targetClass="com.htmake.readerpro.MainActivity" />
</shortcut>
</shortcuts>
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import java.net.ServerSocket
import java.net.InetAddress
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@@ -6,9 +6,9 @@ import org.junit.Test
class MediaLaunchRouteTest {
@Test fun shortcutsSelectOnlyFixedMediaRoutes() {
assertEquals("#/media/video", MediaLaunchRoute.fragmentForAction("cool.cnb.reader.OPEN_VIDEO"))
assertEquals("#/media/music", MediaLaunchRoute.fragmentForAction("cool.cnb.reader.OPEN_MUSIC"))
assertEquals("#/media/audiobook", MediaLaunchRoute.fragmentForAction("cool.cnb.reader.OPEN_AUDIOBOOK"))
assertEquals("#/media/video", MediaLaunchRoute.fragmentForAction("com.htmake.readerpro.OPEN_VIDEO"))
assertEquals("#/media/music", MediaLaunchRoute.fragmentForAction("com.htmake.readerpro.OPEN_MUSIC"))
assertEquals("#/media/audiobook", MediaLaunchRoute.fragmentForAction("com.htmake.readerpro.OPEN_AUDIOBOOK"))
}
@Test fun normalLaunchAndUntrustedActionsDoNotRedirectReading() {
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import java.io.File
import java.net.HttpURLConnection
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import java.net.ServerSocket
import java.net.InetAddress
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import org.json.JSONObject
import org.junit.Assert.*
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import org.json.JSONObject
import org.junit.Assert.*
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import androidx.media3.common.Player
import java.lang.reflect.Proxy
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import org.junit.Assert.*
import org.junit.Test
@@ -1,4 +1,4 @@
package cool.cnb.reader.media
package com.htmake.readerpro.media
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
+7 -7
View File
@@ -32,14 +32,14 @@ done
# 2. Every import of our own code must resolve to a file that exists.
note "==> checking project imports resolve"
for file in $(find "$android_dir/app/src" -name '*.kt'); do
sed -n 's/^import cool\.cnb\.reader\.\(.*\)$/\1/p' "$file" | while read -r symbol; do
sed -n 's/^import com\.htmake\.readerpro\.\(.*\)$/\1/p' "$file" | while read -r symbol; do
# `BuildConfig` is generated, and a class name is the last dot segment.
case "$symbol" in
BuildConfig) continue ;;
esac
path="$android_dir/app/src/main/java/cool/cnb/reader/$(printf '%s' "$symbol" | tr '.' '/').kt"
path="$android_dir/app/src/main/java/com/htmake/readerpro/$(printf '%s' "$symbol" | tr '.' '/').kt"
if [ ! -f "$path" ]; then
err "$file imports cool.cnb.reader.$symbol but $path does not exist"
err "$file imports com.htmake.readerpro.$symbol but $path does not exist"
fi
done
done
@@ -55,8 +55,8 @@ done
# methods against `ReaderBridge.kt` is what produced eleven false failures the
# first time this was run.
note "==> checking the JS bridge contract in both directions"
bridge_file="$android_dir/app/src/main/java/cool/cnb/reader/bridge/ReaderBridge.kt"
speech_file="$android_dir/app/src/main/java/cool/cnb/reader/bridge/SpeechBridge.kt"
bridge_file="$android_dir/app/src/main/java/com/htmake/readerpro/bridge/ReaderBridge.kt"
speech_file="$android_dir/app/src/main/java/com/htmake/readerpro/bridge/SpeechBridge.kt"
types_file="$repo_dir/web/src/android-bridge.d.ts"
# A method is "implemented" if either bridge class exposes it to the page: a
@@ -119,7 +119,7 @@ note "==> checking the render-mode vocabulary matches"
web_modes=$(sed -n "s/^export type RenderMode = \(.*\);/\1/p" "$repo_dir/web/src/formats/types.ts" \
| tr -d "'" | tr -d ' ' | tr '|' '\n' | sort -u)
shell_modes=$(sed -n 's/.*mode != "\([a-z]*\)".*/\1/p' \
"$android_dir/app/src/main/java/cool/cnb/reader/bridge/ReaderBridge.kt" | sort -u)
"$android_dir/app/src/main/java/com/htmake/readerpro/bridge/ReaderBridge.kt" | sort -u)
if [ -z "$web_modes" ]; then
err "cannot read RenderMode from web/src/formats/types.ts"
fi
@@ -130,7 +130,7 @@ for mode in $shell_modes; do
done
# The shell draws images natively, so it must know the client's image vocabulary.
decodable=$(sed -n '/private val DECODABLE = setOf(/,/)/p' \
"$android_dir/app/src/main/java/cool/cnb/reader/web/NativePageView.kt" \
"$android_dir/app/src/main/java/com/htmake/readerpro/web/NativePageView.kt" \
| sed -n 's/.*"\(image\/[a-z]*\)".*/\1/p' | sort -u)
for mediaType in "image/jpeg" "image/png" "image/webp"; do
if ! printf '%s\n' "$decodable" | grep -qx "$mediaType"; then
+3 -3
View File
@@ -6,9 +6,9 @@
# automatically becomes the instance admin.
services:
reader:
# CI publishes branch and commit tags; it does not publish :latest.
# Pin READER_IMAGE to a published commit tag for repeatable upgrades.
image: ${READER_IMAGE:-cnb.cool/hectorqin/reader:main}
# Releases (v* tags) publish x.y.z, x.y, latest and sha-* tags to Docker Hub.
# Pin READER_IMAGE to a release version for repeatable upgrades.
image: ${READER_IMAGE:-hectorqin/reader-pro:latest}
# To build from source instead of pulling the published image. The context is
# the repository root because the image bakes in the H5 client built from
# ./web as well as the server.
+3 -5
View File
@@ -8,13 +8,11 @@
# fails, so the client stage is gated behind a build arg and the runtime stage
# tolerates an absent bundle. A server-only deployment stays possible.
#
# The base image is overridable because `docker.cnb.cool` is an authenticated
# mirror: a plain `docker build` on a machine whose token has no pull rights on
# it fails at the very first stage. Docker Hub is the default; pass the mirror
# where it is reachable (CNB pipelines, or anywhere Hub is slow):
# The base image is overridable so a build can pull Node from a registry mirror
# where Docker Hub is slow or unreachable. Docker Hub is the default:
#
# docker build . -f server/Dockerfile \
# --build-arg NODE_IMAGE=docker.cnb.cool/dockerhub/node:24-alpine
# --build-arg NODE_IMAGE=<mirror>/node:24-alpine
ARG NODE_IMAGE=node:24-alpine
FROM ${NODE_IMAGE} AS web