diff --git a/.gitignore b/.gitignore index 73ec0e8..df1bc09 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ android/app/src/main/assets/www/ android/keystore/ *.jks *.keystore -PhoneViewer-*.apk +EasyView-*.apk *.dwg *.DWG .env diff --git a/README.md b/README.md index e0b0233..325ae2b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PhoneViewer / 万能看图 +# EasyView / 万能看图 手机 / 平板上的本地 DWG、PDF、EPUB、FB2、XPS/OXPS、CBZ 和图片浏览器。 @@ -19,16 +19,25 @@ npm run build ## 构建 Android APK -需要 JDK 17、Android SDK 34 和 Gradle 的 Kotlin 编译器(脚本位于 `E:\AiCodeGitea\ven\AndroidDev`,可在 `scripts/build-apk.mjs` 中调整路径)。 +需要 JDK 17+/21、Android SDK(platform 34、build-tools)以及带 Kotlin 编译器的 Gradle 发行版。 +本工作区已把这些工具链放在 `../ven/` 下,脚本会自动探测: -签名信息不写入仓库。构建前设置: +| 工具 | 位置 | +| --- | --- | +| JDK | `ven/jdk21`(或 `JAVA_HOME`) | +| Android SDK | `ven/android-sdk`(或 `ANDROID_HOME`) | +| Gradle + Kotlin 编译器的 lib | `ven/gradle-dist/gradle-8.9`(或 `GRADLE_HOME`) | + +签名信息不写入仓库,首次构建会自动生成 `android/keystore/easyview.jks`。构建前设置口令: ```bash -PHONEVIEWER_KEYSTORE_PASS=your-keystore-password -PHONEVIEWER_KEY_PASS=your-key-password +source ../ven/node-env.sh # Node 工具链(可选) +export EASYVIEW_KEYSTORE_PASS=your-keystore-password +export EASYVIEW_KEY_PASS=your-key-password # 省略则与上面相同 npm run apk ``` +产物:`EasyView-.apk`(仓库根目录),`android/build/` 为中间产物。 `android/keystore/`、测试 DWG、APK、`dist/`、`node_modules/` 均已加入 `.gitignore`。图纸不会上传,解析完全在本地进行。 ## 许可 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 94bea7b..0e20ea9 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - 万能看图 + EasyView diff --git a/build-apk.sh b/build-apk.sh new file mode 100755 index 0000000..dfd2aae --- /dev/null +++ b/build-apk.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# 一键构建 EasyView APK(Linux / 本工作区 ven 工具链) +# 用法:./build-apk.sh +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ven="$(cd "$here/../ven" && pwd)" + +source "$ven/node-env.sh" +export npm_config_cache="$ven/npm" +export ANDROID_HOME="$ven/android-sdk" +export ANDROID_SDK_ROOT="$ANDROID_HOME" + +# 签名口令:优先取 android/keystore/.env.keystore(已 gitignore) +if [[ -f "$here/android/keystore/.env.keystore" ]]; then + set -a + # shellcheck disable=SC1091 + . "$here/android/keystore/.env.keystore" + set +a +fi +: "${EASYVIEW_KEYSTORE_PASS:?未设置签名口令:请在 android/keystore/.env.keystore 中写入 EASYVIEW_KEYSTORE_PASS}" + +cd "$here" +node scripts/build-apk.mjs diff --git a/index.html b/index.html index 474e7de..d1a468d 100644 --- a/index.html +++ b/index.html @@ -10,17 +10,17 @@ - + - 万能看图 · DWG/PDF 浏览器 + EasyView · DWG/PDF 浏览器
- + diff --git a/package-lock.json b/package-lock.json index f6ed1a9..544b57c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2083,9 +2083,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2307,9 +2304,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2324,9 +2318,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2341,9 +2332,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2358,9 +2346,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2375,9 +2360,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2392,9 +2374,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2409,9 +2388,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2426,9 +2402,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2443,9 +2416,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2460,9 +2430,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2477,9 +2444,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2494,9 +2458,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2511,9 +2472,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/scripts/build-apk.mjs b/scripts/build-apk.mjs index a8e6fcf..0563ef2 100644 --- a/scripts/build-apk.mjs +++ b/scripts/build-apk.mjs @@ -4,19 +4,92 @@ import { execFileSync } from 'node:child_process' import AdmZip from 'adm-zip' const root = process.cwd() -const devRoot = process.env.ANDROID_DEV_ROOT || 'E:\\AiCodeGitea\\ven\\AndroidDev' -const jdkHome = fs.existsSync(path.join(process.env.JAVA_HOME || '', 'bin', 'java.exe')) - ? process.env.JAVA_HOME - : path.join(devRoot, 'jdk17') -const sdkHome = fs.existsSync(path.join(process.env.ANDROID_HOME || '', 'platforms')) - ? process.env.ANDROID_HOME - : path.join(devRoot, 'Sdk') -const gradleHome = path.join(devRoot, 'gradle-8.7') -const buildTools = path.join(sdkHome, 'build-tools', '34.0.0') -const androidJar = path.join(sdkHome, 'platforms', 'android-34', 'android.jar') -const java = path.join(jdkHome, 'bin', 'java.exe') -const jar = path.join(jdkHome, 'bin', 'jar.exe') -const keytool = path.join(jdkHome, 'bin', 'keytool.exe') +const isWin = process.platform === 'win32' +const exeSuffix = isWin ? '.exe' : '' +const devRoot = + process.env.ANDROID_DEV_ROOT || + (isWin ? 'E:\\AiCodeGitea\\ven\\AndroidDev' : path.resolve(root, '..', 'ven')) + +const isFile = (p) => { + try { + return fs.statSync(p).isFile() + } catch { + return false + } +} +const isDir = (p) => { + try { + return fs.statSync(p).isDirectory() + } catch { + return false + } +} + +const jdkHome = (() => { + const candidates = [ + process.env.JAVA_HOME, + path.join(devRoot, 'jdk21'), + path.join(devRoot, 'jdk17'), + path.join(devRoot, 'AndroidDev', 'jdk17') + ].filter(Boolean) + const found = candidates.find((dir) => isFile(path.join(dir, 'bin', `java${exeSuffix}`))) + if (!found) throw new Error('JDK not found: set JAVA_HOME (workspace: source ../ven/*-env.sh)') + return found +})() + +const sdkHome = (() => { + const candidates = [ + process.env.ANDROID_HOME, + process.env.ANDROID_SDK_ROOT, + path.join(devRoot, 'android-sdk'), + path.join(devRoot, 'Sdk') + ].filter(Boolean) + const found = candidates.find((dir) => isDir(path.join(dir, 'platforms'))) + if (!found) throw new Error('Android SDK not found: set ANDROID_HOME') + return found +})() + +function findGradleHome() { + const explicit = [process.env.GRADLE_HOME, path.join(devRoot, 'gradle-8.7')].filter(Boolean) + const found = explicit.find((dir) => isFile(path.join(dir, 'lib', 'kotlin-stdlib-1.9.23.jar'))) + if (found) return found + const distRoot = path.join(devRoot, 'gradle-dist') + const candidates = isDir(distRoot) + ? fs + .readdirSync(distRoot) + .filter((name) => name.startsWith('gradle-')) + .sort() + .reverse() + .map((name) => path.join(distRoot, name)) + : [] + const hit = candidates.find((dir) => isDir(path.join(dir, 'lib'))) + if (!hit) throw new Error(`Gradle distribution with Kotlin compiler not found under ${distRoot}`) + return hit +} +const gradleHome = findGradleHome() + +const buildToolsVersion = + process.env.BUILD_TOOLS_VERSION || + fs + .readdirSync(path.join(sdkHome, 'build-tools')) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + .pop() +const buildTools = path.join(sdkHome, 'build-tools', buildToolsVersion) + +const platformId = + process.env.COMPILE_SDK || + (isDir(path.join(sdkHome, 'platforms', 'android-34')) + ? 'android-34' + : fs + .readdirSync(path.join(sdkHome, 'platforms')) + .filter((name) => name.startsWith('android-')) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + .pop()) +const androidJar = path.join(sdkHome, 'platforms', platformId, 'android.jar') + +const java = path.join(jdkHome, 'bin', `java${exeSuffix}`) +const jar = path.join(jdkHome, 'bin', `jar${exeSuffix}`) +const keytool = path.join(jdkHome, 'bin', `keytool${exeSuffix}`) const kotlincJars = path.join(gradleHome, 'lib', '*') const kotlinStdlib = fs .readdirSync(path.join(gradleHome, 'lib')) @@ -25,6 +98,9 @@ const kotlinStdlib = fs .pop() if (!kotlinStdlib) throw new Error('kotlin-stdlib not found in Gradle lib') +const appVersion = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version +const versionCode = Number(process.env.APP_VERSION_CODE || 7) + const env = { ...process.env, JAVA_HOME: jdkHome, @@ -35,9 +111,13 @@ const env = { buildTools, path.join(sdkHome, 'platform-tools'), process.env.PATH || '' - ].join(';') + ].join(path.delimiter) } +console.log(`JDK : ${jdkHome}`) +console.log(`Android SDK: ${sdkHome} (build-tools ${buildToolsVersion}, platform ${platformId})`) +console.log(`Gradle lib : ${gradleHome}`) + const appDir = path.join(root, 'android', 'app', 'src', 'main') const manifest = path.join(appDir, 'AndroidManifest.xml') const resDir = path.join(appDir, 'res') @@ -47,15 +127,17 @@ const sourceDir = path.join(appDir, 'java') const outDir = path.join(root, 'android', 'build') const classesDir = path.join(outDir, 'classes') const dexDir = path.join(outDir, 'dex') -const apkName = 'PhoneViewer-0.5.0.apk' +const apkName = `EasyView-${appVersion}.apk` function run(command, args, options = {}) { console.log('>', path.basename(command), args.join(' ')) execFileSync(command, args, { stdio: 'inherit', env, ...options }) } -function runBat(batFile, args, options = {}) { - run('cmd.exe', ['/c', batFile, ...args], options) +// Android build-tools ship .bat wrappers on Windows and plain scripts elsewhere. +function runTool(name, args, options = {}) { + const tool = path.join(buildTools, `${name}${isWin ? '.bat' : ''}`) + return isWin ? run('cmd.exe', ['/c', tool, ...args], options) : run(tool, args, options) } fs.rmSync(outDir, { recursive: true, force: true }) @@ -64,7 +146,8 @@ fs.mkdirSync(classesDir, { recursive: true }) fs.mkdirSync(dexDir, { recursive: true }) console.log('== building web assets ==') -run('cmd.exe', ['/c', 'npm run build'], { cwd: root }) +if (isWin) run('cmd.exe', ['/c', 'npm run build'], { cwd: root }) +else run(process.env.NPM_BIN || 'npm', ['run', 'build'], { cwd: root, shell: false }) console.log('== copying web assets into Android assets ==') fs.rmSync(wwwDir, { recursive: true, force: true }) @@ -93,7 +176,7 @@ run(java, [ '-no-stdlib', '-no-reflect', '-classpath', - `${androidJar};${path.join(gradleHome, 'lib', kotlinStdlib)}`, + [androidJar, path.join(gradleHome, 'lib', kotlinStdlib)].join(path.delimiter), '-jvm-target', '17', '-d', @@ -106,7 +189,7 @@ const classesJar = path.join(outDir, 'classes.jar') run(jar, ['cf', classesJar, '-C', classesDir, '.']) console.log('== dexing with d8 ==') -runBat(path.join(buildTools, 'd8.bat'), [ +runTool('d8', [ '--release', '--lib', androidJar, @@ -120,11 +203,11 @@ runBat(path.join(buildTools, 'd8.bat'), [ console.log('== compiling resources ==') const resZip = path.join(outDir, 'resources.zip') -run(path.join(buildTools, 'aapt2.exe'), ['compile', '-o', resZip, '--dir', resDir]) +run(path.join(buildTools, `aapt2${exeSuffix}`), ['compile', '-o', resZip, '--dir', resDir]) console.log('== linking base apk ==') const baseApk = path.join(outDir, 'base.apk') -run(path.join(buildTools, 'aapt2.exe'), [ +run(path.join(buildTools, `aapt2${exeSuffix}`), [ 'link', '-o', baseApk, @@ -139,9 +222,9 @@ run(path.join(buildTools, 'aapt2.exe'), [ '--target-sdk-version', '34', '--version-code', - '6', + String(versionCode), '--version-name', - '0.5.0', + appVersion, '--auto-add-overlay' ]) @@ -167,17 +250,17 @@ zip.writeZip(unsigned) console.log('== zipalign ==') const aligned = path.join(outDir, 'aligned.apk') -run(path.join(buildTools, 'zipalign.exe'), ['-p', '-f', '4', unsigned, aligned]) +run(path.join(buildTools, `zipalign${exeSuffix}`), ['-p', '-f', '4', unsigned, aligned]) console.log('== signing ==') const keystoreDir = path.join(root, 'android', 'keystore') -const keystore = path.join(keystoreDir, 'phoneviewer.jks') -const storePass = process.env.PHONEVIEWER_KEYSTORE_PASS -const keyPass = process.env.PHONEVIEWER_KEY_PASS ?? storePass +const keystore = path.join(keystoreDir, 'easyview.jks') +const storePass = process.env.EASYVIEW_KEYSTORE_PASS ?? process.env.PHONEVIEWER_KEYSTORE_PASS +const keyPass = process.env.EASYVIEW_KEY_PASS ?? process.env.PHONEVIEWER_KEY_PASS ?? storePass if (!storePass) { - throw new Error('Set PHONEVIEWER_KEYSTORE_PASS before building the signed APK.') + throw new Error('Set EASYVIEW_KEYSTORE_PASS before building the signed APK.') } -const alias = 'phoneviewer' +const alias = 'easyview' if (!fs.existsSync(keystore)) { fs.mkdirSync(keystoreDir, { recursive: true }) run(keytool, [ @@ -200,12 +283,12 @@ if (!fs.existsSync(keystore)) { '-keypass', keyPass, '-dname', - 'CN=PhoneViewer, OU=Android, O=PhoneViewer, L=Beijing, ST=Beijing, C=CN', + 'CN=EasyView, OU=Android, O=EasyView, L=Beijing, ST=Beijing, C=CN', '-noprompt' ]) } const finalApk = path.join(outDir, apkName) -runBat(path.join(buildTools, 'apksigner.bat'), [ +runTool('apksigner', [ 'sign', '--ks', keystore, @@ -219,7 +302,7 @@ runBat(path.join(buildTools, 'apksigner.bat'), [ finalApk, aligned ]) -runBat(path.join(buildTools, 'apksigner.bat'), ['verify', '--verbose', '--print-certs', finalApk]) +runTool('apksigner', ['verify', '--verbose', '--print-certs', finalApk]) const rootApk = path.join(root, apkName) fs.copyFileSync(finalApk, rootApk) diff --git a/src/core/app.ts b/src/core/app.ts index 1b4e649..0a7f8fb 100644 --- a/src/core/app.ts +++ b/src/core/app.ts @@ -117,7 +117,7 @@ function buildHome(): HTMLElement { 'div', { class: 'home-brand' }, h('div', { class: 'brand-mark', html: LOGO_SVG }), - h('div', { class: 'brand-text' }, h('div', { class: 'brand-name', text: '万能看图' })) + h('div', { class: 'brand-text' }, h('div', { class: 'brand-name', text: 'EasyView' })) ) const top = h( @@ -275,7 +275,7 @@ async function openFile(file: File): Promise { viewerRoot.appendChild(host.root) viewerRoot.hidden = false homeEl.hidden = true - document.title = `${file.name} · 万能看图` + document.title = `${file.name} · EasyView` await new Promise((resolve) => requestAnimationFrame(() => resolve())) let createdViewer: ActiveViewer | null = null @@ -326,7 +326,7 @@ async function openFile(file: File): Promise { activeViewer = null viewerRoot.hidden = true homeEl.hidden = false - document.title = '万能看图 · DWG/PDF 浏览器' + document.title = 'EasyView · DWG/PDF 浏览器' const message = error instanceof Error ? error.message : String(error) console.error(error) modalMessage( @@ -353,7 +353,7 @@ function closeActiveViewer(): void { viewerRoot.innerHTML = '' viewerRoot.hidden = true homeEl.hidden = false - document.title = '万能看图 · DWG/PDF 浏览器' + document.title = 'EasyView · DWG/PDF 浏览器' void refreshRecent() } diff --git a/src/doc/docViewer.ts b/src/doc/docViewer.ts index 05e41ff..de1273f 100644 --- a/src/doc/docViewer.ts +++ b/src/doc/docViewer.ts @@ -315,7 +315,7 @@ export class DocViewer { add('rotate', 'rotate-cw', '旋转 90°', () => this.rotatePage()) add('invert', 'moon', '反色阅读', () => this.toggleInvert()) this.fullscreenBtn = add('fullscreen', 'maximize', '全屏', () => void this.toggleFullscreen()) - add('about', 'info', '关于万能看图', () => showAbout()) + add('about', 'info', '关于 EasyView', () => showAbout()) return menu } diff --git a/src/ui/about.ts b/src/ui/about.ts index 6aa78e1..2aa94ab 100644 --- a/src/ui/about.ts +++ b/src/ui/about.ts @@ -10,7 +10,7 @@ export function showAbout(): void { h( 'p', { class: 'modal-text' }, - '万能看图是一个完全在本地运行的手机 / 平板图纸与文档浏览器。文件只在本机解析,不会上传到任何服务器。' + 'EasyView 是一个完全在本地运行的手机 / 平板图纸与文档浏览器。文件只在本机解析,不会上传到任何服务器。' ), h('h3', { text: '支持的格式' }), h( @@ -36,12 +36,12 @@ export function showAbout(): void { h('li', {}, '文档渲染:MuPDF.js(SumatraPDF 同源渲染内核家族)— AGPL-3.0'), h('li', {}, '本应用以 AGPL-3.0 发布;完整许可文本见项目 LICENSE 文件。') ), - h('p', { class: 'about-version', text: `万能看图 v${VERSION} · PWA · 本地解析` }) + h('p', { class: 'about-version', text: `EasyView v${VERSION} · PWA · 本地解析` }) ) const close = h('button', { class: 'btn btn-primary', type: 'button', text: '关闭' }) const modal = openModal({ - title: '关于万能看图', + title: '关于 EasyView', subtitle: '手机 / 平板上的 DWG 图纸 & 文档浏览器', content: body, actions: [close], diff --git a/src/ui/guide.ts b/src/ui/guide.ts index 67c725d..9480736 100644 --- a/src/ui/guide.ts +++ b/src/ui/guide.ts @@ -14,7 +14,7 @@ export function showGuide(): void { h( 'p', { class: 'modal-text' }, - '万能看图把 DWG 图纸浏览和文档阅读放在同一个本地 App 里。文件只在本机解析,不会上传到服务器。' + 'EasyView 把 DWG 图纸浏览和文档阅读放在同一个本地 App 里。文件只在本机解析,不会上传到服务器。' ), section( '支持格式', diff --git a/vite.config.ts b/vite.config.ts index df2d833..a0ee91e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,8 +16,8 @@ export default defineConfig({ injectRegister: false, includeAssets: ['icons/favicon.svg', 'icons/apple-touch-icon.png'], manifest: { - name: '万能看图 · DWG/PDF 浏览', - short_name: '万能看图', + name: 'EasyView · DWG/PDF 浏览', + short_name: 'EasyView', description: '手机与平板上的 DWG 图纸浏览器,支持模型空间与布局空间、图层控制、测量,以及 PDF / EPUB / XPS / CBZ / 图片阅读。', lang: 'zh-CN',